mirror of
https://github.com/jcwimer/wrestlingApp
synced 2026-04-11 16:01:56 +00:00
Compare commits
9 Commits
dependabot
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| 970f38ed14 | |||
| 7526148ba5 | |||
| e8e0fa291b | |||
| 679fc2fcb9 | |||
| 18d39c6c8f | |||
| ca4d5ce0db | |||
| 654cb84827 | |||
| dc50efe8fc | |||
| 8670ce38c3 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -21,6 +21,7 @@ tmp
|
|||||||
.rvmrc
|
.rvmrc
|
||||||
deploy/prod.env
|
deploy/prod.env
|
||||||
frontend/node_modules
|
frontend/node_modules
|
||||||
|
node_modules
|
||||||
.aider*
|
.aider*
|
||||||
|
|
||||||
# Ignore cypress test results
|
# Ignore cypress test results
|
||||||
@@ -34,3 +35,6 @@ cypress-tests/cypress/videos
|
|||||||
|
|
||||||
# generated by cine mcp settings
|
# generated by cine mcp settings
|
||||||
~/
|
~/
|
||||||
|
|
||||||
|
/.ruby-lsp
|
||||||
|
.codex
|
||||||
|
|||||||
11
AGENTS.md
Normal file
11
AGENTS.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
- I have two ways to run rails commands in the repo. Either use rvm with `rvm use 4.0.1; rvm gemset use wrestlingdev;` or use docker with `docker run -it -v $(pwd):/rails wrestlingdev-dev <rails command>`
|
||||||
|
- If the docker image doesn't exist, use the build command: `docker build -t wrestlingdev-dev -f deploy/rails-dev-Dockerfile .`
|
||||||
|
- If the Gemfile changes, you need to rebuild the docker image: `docker build -t wrestlingdev-dev -f deploy/rails-dev-Dockerfile .`
|
||||||
|
- Do not add unnecessary comments to the code where you remove things.
|
||||||
|
- Write as little code as possible. I do not want crazy non standard rails implementations.
|
||||||
|
- This project is using propshaft and importmap.
|
||||||
|
- Stimulus is used for javascript.
|
||||||
|
- Cypress tests are created for js tests. They can be found in cypress-tests/cypress
|
||||||
|
- Cypress tests can be run with docker: bash cypress-tests/run-cypress-tests.sh
|
||||||
|
- javascript tests are through vitest. See `vitest.config.js`. Run `npm run test:js`
|
||||||
|
- importmap pins in `importmap.rb` and aliases in `vitest.config.js` need to match.
|
||||||
@@ -5,7 +5,6 @@ import "@hotwired/turbo-rails";
|
|||||||
import { createConsumer } from "@rails/actioncable"; // Import createConsumer directly
|
import { createConsumer } from "@rails/actioncable"; // Import createConsumer directly
|
||||||
import "jquery";
|
import "jquery";
|
||||||
import "bootstrap";
|
import "bootstrap";
|
||||||
import "datatables.net";
|
|
||||||
|
|
||||||
// Stimulus setup
|
// Stimulus setup
|
||||||
import { Application } from "@hotwired/stimulus";
|
import { Application } from "@hotwired/stimulus";
|
||||||
@@ -19,13 +18,21 @@ window.Stimulus = application;
|
|||||||
import WrestlerColorController from "controllers/wrestler_color_controller";
|
import WrestlerColorController from "controllers/wrestler_color_controller";
|
||||||
import MatchScoreController from "controllers/match_score_controller";
|
import MatchScoreController from "controllers/match_score_controller";
|
||||||
import MatchDataController from "controllers/match_data_controller";
|
import MatchDataController from "controllers/match_data_controller";
|
||||||
|
import MatchStateController from "controllers/match_state_controller";
|
||||||
|
import MatchScoreboardController from "controllers/match_scoreboard_controller";
|
||||||
|
import MatStateController from "controllers/mat_state_controller";
|
||||||
import MatchSpectateController from "controllers/match_spectate_controller";
|
import MatchSpectateController from "controllers/match_spectate_controller";
|
||||||
|
import UpMatchesConnectionController from "controllers/up_matches_connection_controller";
|
||||||
|
|
||||||
// Register controllers
|
// Register controllers
|
||||||
application.register("wrestler-color", WrestlerColorController);
|
application.register("wrestler-color", WrestlerColorController);
|
||||||
application.register("match-score", MatchScoreController);
|
application.register("match-score", MatchScoreController);
|
||||||
application.register("match-data", MatchDataController);
|
application.register("match-data", MatchDataController);
|
||||||
|
application.register("match-state", MatchStateController);
|
||||||
|
application.register("match-scoreboard", MatchScoreboardController);
|
||||||
|
application.register("mat-state", MatStateController);
|
||||||
application.register("match-spectate", MatchSpectateController);
|
application.register("match-spectate", MatchSpectateController);
|
||||||
|
application.register("up-matches-connection", UpMatchesConnectionController);
|
||||||
|
|
||||||
// Your existing Action Cable consumer setup
|
// Your existing Action Cable consumer setup
|
||||||
(function() {
|
(function() {
|
||||||
@@ -39,7 +46,7 @@ application.register("match-spectate", MatchSpectateController);
|
|||||||
}
|
}
|
||||||
}).call(this);
|
}).call(this);
|
||||||
|
|
||||||
console.log("Propshaft/Importmap application.js initialized with jQuery, Bootstrap, Stimulus, and DataTables.");
|
console.log("Propshaft/Importmap application.js initialized with jQuery, Bootstrap, and Stimulus.");
|
||||||
|
|
||||||
// If you have custom JavaScript files in app/javascript/ that were previously
|
// If you have custom JavaScript files in app/javascript/ that were previously
|
||||||
// handled by Sprockets `require_tree`, you'll need to import them here explicitly.
|
// handled by Sprockets `require_tree`, you'll need to import them here explicitly.
|
||||||
|
|||||||
163
app/assets/javascripts/controllers/mat_state_controller.js
Normal file
163
app/assets/javascripts/controllers/mat_state_controller.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static values = {
|
||||||
|
tournamentId: Number,
|
||||||
|
matId: Number,
|
||||||
|
boutNumber: Number,
|
||||||
|
matchId: Number,
|
||||||
|
selectMatchUrl: String,
|
||||||
|
weightLabel: String,
|
||||||
|
w1Id: Number,
|
||||||
|
w2Id: Number,
|
||||||
|
w1Name: String,
|
||||||
|
w2Name: String,
|
||||||
|
w1School: String,
|
||||||
|
w2School: String
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.boundHandleSubmit = this.handleSubmit.bind(this)
|
||||||
|
this.saveSelectedBout()
|
||||||
|
this.broadcastSelectedBout()
|
||||||
|
this.element.addEventListener("submit", this.boundHandleSubmit)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.element.removeEventListener("submit", this.boundHandleSubmit)
|
||||||
|
}
|
||||||
|
|
||||||
|
storageKey() {
|
||||||
|
return `mat-selected-bout:${this.tournamentIdValue}:${this.matIdValue}`
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSelectedBout() {
|
||||||
|
if (!this.matIdValue || this.matIdValue <= 0) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(this.storageKey(), JSON.stringify({
|
||||||
|
boutNumber: this.boutNumberValue,
|
||||||
|
matchId: this.matchIdValue,
|
||||||
|
updatedAt: Date.now()
|
||||||
|
}))
|
||||||
|
} catch (_error) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastSelectedBout() {
|
||||||
|
if (!this.hasSelectMatchUrlValue || !this.selectMatchUrlValue) return
|
||||||
|
|
||||||
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
|
||||||
|
const body = new URLSearchParams()
|
||||||
|
if (this.matchIdValue) body.set("match_id", this.matchIdValue.toString())
|
||||||
|
if (this.boutNumberValue) body.set("bout_number", this.boutNumberValue.toString())
|
||||||
|
|
||||||
|
const lastMatchResult = this.loadLastMatchResult()
|
||||||
|
if (lastMatchResult) body.set("last_match_result", lastMatchResult)
|
||||||
|
|
||||||
|
fetch(this.selectMatchUrlValue, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"X-CSRF-Token": csrfToken || "",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
|
"Accept": "text/vnd.turbo-stream.html, text/html, application/xhtml+xml"
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
credentials: "same-origin"
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMatchResultStorageKey() {
|
||||||
|
return `mat-last-match-result:${this.tournamentIdValue}:${this.matIdValue}`
|
||||||
|
}
|
||||||
|
|
||||||
|
saveLastMatchResult(text) {
|
||||||
|
if (!this.matIdValue || this.matIdValue <= 0) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (text) {
|
||||||
|
window.localStorage.setItem(this.lastMatchResultStorageKey(), text)
|
||||||
|
} else {
|
||||||
|
window.localStorage.removeItem(this.lastMatchResultStorageKey())
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadLastMatchResult() {
|
||||||
|
try {
|
||||||
|
return window.localStorage.getItem(this.lastMatchResultStorageKey()) || ""
|
||||||
|
} catch (_error) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(event) {
|
||||||
|
const form = event.target
|
||||||
|
if (!(form instanceof HTMLFormElement)) return
|
||||||
|
|
||||||
|
const resultText = this.buildLastMatchResult(form)
|
||||||
|
if (!resultText) return
|
||||||
|
|
||||||
|
this.saveLastMatchResult(resultText)
|
||||||
|
this.broadcastCurrentState(resultText)
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastCurrentState(lastMatchResult) {
|
||||||
|
if (!this.hasSelectMatchUrlValue || !this.selectMatchUrlValue) return
|
||||||
|
|
||||||
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
|
||||||
|
const body = new URLSearchParams()
|
||||||
|
if (this.matchIdValue) body.set("match_id", this.matchIdValue.toString())
|
||||||
|
if (this.boutNumberValue) body.set("bout_number", this.boutNumberValue.toString())
|
||||||
|
if (lastMatchResult) body.set("last_match_result", lastMatchResult)
|
||||||
|
|
||||||
|
fetch(this.selectMatchUrlValue, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"X-CSRF-Token": csrfToken || "",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
|
"Accept": "text/vnd.turbo-stream.html, text/html, application/xhtml+xml"
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
credentials: "same-origin",
|
||||||
|
keepalive: true
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
buildLastMatchResult(form) {
|
||||||
|
const winnerId = form.querySelector("#match_winner_id")?.value
|
||||||
|
const winType = form.querySelector("#match_win_type")?.value
|
||||||
|
const score = form.querySelector("#final-score-field")?.value
|
||||||
|
if (!winnerId || !winType) return ""
|
||||||
|
|
||||||
|
const winner = this.participantDataForId(winnerId)
|
||||||
|
const loser = this.loserParticipantData(winnerId)
|
||||||
|
if (!winner || !loser) return ""
|
||||||
|
|
||||||
|
const weightLabel = this.hasWeightLabelValue ? this.weightLabelValue : ""
|
||||||
|
return `${weightLabel} lbs - ${winner.name} (${winner.school}) ${winType} ${loser.name} (${loser.school}) ${score || ""}`.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
participantDataForId(participantId) {
|
||||||
|
const normalizedId = String(participantId)
|
||||||
|
if (normalizedId === String(this.w1IdValue)) {
|
||||||
|
return { name: this.w1NameValue, school: this.w1SchoolValue }
|
||||||
|
}
|
||||||
|
if (normalizedId === String(this.w2IdValue)) {
|
||||||
|
return { name: this.w2NameValue, school: this.w2SchoolValue }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
loserParticipantData(winnerId) {
|
||||||
|
const normalizedId = String(winnerId)
|
||||||
|
if (normalizedId === String(this.w1IdValue)) {
|
||||||
|
return { name: this.w2NameValue, school: this.w2SchoolValue }
|
||||||
|
}
|
||||||
|
if (normalizedId === String(this.w2IdValue)) {
|
||||||
|
return { name: this.w1NameValue, school: this.w1SchoolValue }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,25 +2,44 @@ import { Controller } from "@hotwired/stimulus"
|
|||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = [
|
static targets = [
|
||||||
"winType", "winnerSelect", "submitButton", "dynamicScoreInput",
|
"winType", "overtimeSelect", "winnerSelect", "submitButton", "dynamicScoreInput",
|
||||||
"finalScoreField", "validationAlerts", "pinTimeTip"
|
"finalScoreField", "validationAlerts", "pinTimeTip"
|
||||||
]
|
]
|
||||||
|
|
||||||
static values = {
|
static values = {
|
||||||
winnerScore: { type: String, default: "0" },
|
winnerScore: { type: String, default: "0" },
|
||||||
loserScore: { type: String, default: "0" }
|
loserScore: { type: String, default: "0" },
|
||||||
|
pinMinutes: { type: String, default: "0" },
|
||||||
|
pinSeconds: { type: String, default: "00" },
|
||||||
|
manualOverride: { type: Boolean, default: false },
|
||||||
|
finished: { type: Boolean, default: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
console.log("Match score controller connected")
|
console.log("Match score controller connected")
|
||||||
// Use setTimeout to ensure the DOM is fully loaded
|
this.boundMarkManualOverride = this.markManualOverride.bind(this)
|
||||||
|
this.element.addEventListener("input", this.boundMarkManualOverride)
|
||||||
|
this.element.addEventListener("change", this.boundMarkManualOverride)
|
||||||
|
if (this.finishedValue) {
|
||||||
|
this.validateForm()
|
||||||
|
return
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.updateScoreInput()
|
this.updateScoreInput()
|
||||||
this.validateForm()
|
this.validateForm()
|
||||||
}, 50)
|
}, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.element.removeEventListener("input", this.boundMarkManualOverride)
|
||||||
|
this.element.removeEventListener("change", this.boundMarkManualOverride)
|
||||||
|
}
|
||||||
|
|
||||||
winTypeChanged() {
|
winTypeChanged() {
|
||||||
|
if (this.finishedValue) {
|
||||||
|
this.validateForm()
|
||||||
|
return
|
||||||
|
}
|
||||||
this.updateScoreInput()
|
this.updateScoreInput()
|
||||||
this.validateForm()
|
this.validateForm()
|
||||||
}
|
}
|
||||||
@@ -30,6 +49,7 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateScoreInput() {
|
updateScoreInput() {
|
||||||
|
if (this.finishedValue) return
|
||||||
const winType = this.winTypeTarget.value
|
const winType = this.winTypeTarget.value
|
||||||
this.dynamicScoreInputTarget.innerHTML = ""
|
this.dynamicScoreInputTarget.innerHTML = ""
|
||||||
|
|
||||||
@@ -48,6 +68,9 @@ export default class extends Controller {
|
|||||||
this.dynamicScoreInputTarget.appendChild(minuteInput)
|
this.dynamicScoreInputTarget.appendChild(minuteInput)
|
||||||
this.dynamicScoreInputTarget.appendChild(secondInput)
|
this.dynamicScoreInputTarget.appendChild(secondInput)
|
||||||
|
|
||||||
|
minuteInput.querySelector("input").value = this.pinMinutesValue || "0"
|
||||||
|
secondInput.querySelector("input").value = this.pinSecondsValue || "00"
|
||||||
|
|
||||||
// Add event listeners to the new inputs
|
// Add event listeners to the new inputs
|
||||||
const inputs = this.dynamicScoreInputTarget.querySelectorAll("input")
|
const inputs = this.dynamicScoreInputTarget.querySelectorAll("input")
|
||||||
inputs.forEach(input => {
|
inputs.forEach(input => {
|
||||||
@@ -111,6 +134,43 @@ export default class extends Controller {
|
|||||||
this.validateForm()
|
this.validateForm()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyDefaultResults(defaults = {}) {
|
||||||
|
if (this.manualOverrideValue || this.finishedValue) return
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(defaults, "winnerId") && this.hasWinnerSelectTarget) {
|
||||||
|
this.winnerSelectTarget.value = defaults.winnerId ? String(defaults.winnerId) : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(defaults, "overtimeType") && this.hasOvertimeSelectTarget) {
|
||||||
|
const allowedValues = Array.from(this.overtimeSelectTarget.options).map((option) => option.value)
|
||||||
|
this.overtimeSelectTarget.value = allowedValues.includes(defaults.overtimeType) ? defaults.overtimeType : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(defaults, "winnerScore")) {
|
||||||
|
this.winnerScoreValue = String(defaults.winnerScore)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(defaults, "loserScore")) {
|
||||||
|
this.loserScoreValue = String(defaults.loserScore)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(defaults, "pinMinutes")) {
|
||||||
|
this.pinMinutesValue = String(defaults.pinMinutes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(defaults, "pinSeconds")) {
|
||||||
|
this.pinSecondsValue = String(defaults.pinSeconds).padStart(2, "0")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateScoreInput()
|
||||||
|
this.validateForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
markManualOverride(event) {
|
||||||
|
if (!event.isTrusted) return
|
||||||
|
this.manualOverrideValue = true
|
||||||
|
}
|
||||||
|
|
||||||
updatePinTimeScore() {
|
updatePinTimeScore() {
|
||||||
const minuteInput = this.dynamicScoreInputTarget.querySelector("#minutes")
|
const minuteInput = this.dynamicScoreInputTarget.querySelector("#minutes")
|
||||||
const secondInput = this.dynamicScoreInputTarget.querySelector("#seconds")
|
const secondInput = this.dynamicScoreInputTarget.querySelector("#seconds")
|
||||||
|
|||||||
@@ -0,0 +1,364 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
import { getMatchStateConfig } from "match-state-config"
|
||||||
|
import { loadJson } from "match-state-transport"
|
||||||
|
import {
|
||||||
|
buildScoreboardContext,
|
||||||
|
connectionPlan,
|
||||||
|
applyMatchPayloadContext,
|
||||||
|
applyMatPayloadContext,
|
||||||
|
applyStatePayloadContext,
|
||||||
|
matchStorageKey,
|
||||||
|
selectedBoutNumber,
|
||||||
|
selectedBoutStorageKey as selectedBoutStorageKeyFromState,
|
||||||
|
storageChangePlan
|
||||||
|
} from "match-state-scoreboard-state"
|
||||||
|
import {
|
||||||
|
boardColors,
|
||||||
|
emptyBoardViewModel,
|
||||||
|
mainClockRunning as mainClockRunningFromPresenters,
|
||||||
|
nextTimerBannerState,
|
||||||
|
populatedBoardViewModel,
|
||||||
|
timerBannerRenderState
|
||||||
|
} from "match-state-scoreboard-presenters"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = [
|
||||||
|
"redSection",
|
||||||
|
"centerSection",
|
||||||
|
"greenSection",
|
||||||
|
"emptyState",
|
||||||
|
"redName",
|
||||||
|
"redSchool",
|
||||||
|
"redScore",
|
||||||
|
"redTimerIndicator",
|
||||||
|
"greenName",
|
||||||
|
"greenSchool",
|
||||||
|
"greenScore",
|
||||||
|
"greenTimerIndicator",
|
||||||
|
"clock",
|
||||||
|
"periodLabel",
|
||||||
|
"weightLabel",
|
||||||
|
"boutLabel",
|
||||||
|
"timerBanner",
|
||||||
|
"timerBannerLabel",
|
||||||
|
"timerBannerClock",
|
||||||
|
"redStats",
|
||||||
|
"greenStats",
|
||||||
|
"lastMatchResult"
|
||||||
|
]
|
||||||
|
|
||||||
|
static values = {
|
||||||
|
sourceMode: { type: String, default: "localstorage" },
|
||||||
|
displayMode: { type: String, default: "fullscreen" },
|
||||||
|
matchId: Number,
|
||||||
|
matId: Number,
|
||||||
|
tournamentId: Number,
|
||||||
|
initialBoutNumber: Number
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.applyControllerContext(buildScoreboardContext({
|
||||||
|
initialBoutNumber: this.initialBoutNumberValue,
|
||||||
|
matchId: this.matchIdValue
|
||||||
|
}))
|
||||||
|
|
||||||
|
const plan = connectionPlan(this.sourceModeValue, this.currentMatchId)
|
||||||
|
if (plan.useStorageListener) {
|
||||||
|
this.storageListener = this.handleStorageChange.bind(this)
|
||||||
|
window.addEventListener("storage", this.storageListener)
|
||||||
|
}
|
||||||
|
if (plan.loadSelectedBout) {
|
||||||
|
this.loadSelectedBoutNumber()
|
||||||
|
}
|
||||||
|
if (plan.subscribeMat) {
|
||||||
|
this.setupMatSubscription()
|
||||||
|
}
|
||||||
|
if (plan.loadLocalState) {
|
||||||
|
this.loadStateFromLocalStorage()
|
||||||
|
}
|
||||||
|
if (plan.subscribeMatch) {
|
||||||
|
this.setupMatchSubscription(plan.matchId)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.startTicking()
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
if (this.storageListener) {
|
||||||
|
window.removeEventListener("storage", this.storageListener)
|
||||||
|
this.storageListener = null
|
||||||
|
}
|
||||||
|
this.unsubscribeMatSubscription()
|
||||||
|
this.unsubscribeMatchSubscription()
|
||||||
|
if (this.tickInterval) {
|
||||||
|
window.clearInterval(this.tickInterval)
|
||||||
|
this.tickInterval = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupMatSubscription() {
|
||||||
|
if (!window.App || !window.App.cable || !this.matIdValue) return
|
||||||
|
if (this.matSubscription) return
|
||||||
|
|
||||||
|
this.matSubscription = App.cable.subscriptions.create(
|
||||||
|
{
|
||||||
|
channel: "MatScoreboardChannel",
|
||||||
|
mat_id: this.matIdValue
|
||||||
|
},
|
||||||
|
{
|
||||||
|
received: (data) => this.handleMatPayload(data)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribeMatSubscription() {
|
||||||
|
if (this.matSubscription) {
|
||||||
|
this.matSubscription.unsubscribe()
|
||||||
|
this.matSubscription = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupMatchSubscription(matchId) {
|
||||||
|
this.unsubscribeMatchSubscription()
|
||||||
|
if (!window.App || !window.App.cable || !matchId) return
|
||||||
|
|
||||||
|
this.matchSubscription = App.cable.subscriptions.create(
|
||||||
|
{
|
||||||
|
channel: "MatchChannel",
|
||||||
|
match_id: matchId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
connected: () => {
|
||||||
|
this.matchSubscription.perform("request_sync")
|
||||||
|
},
|
||||||
|
received: (data) => {
|
||||||
|
this.handleMatchPayload(data)
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribeMatchSubscription() {
|
||||||
|
if (this.matchSubscription) {
|
||||||
|
this.matchSubscription.unsubscribe()
|
||||||
|
this.matchSubscription = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMatPayload(data) {
|
||||||
|
const nextContext = applyMatPayloadContext(this.currentContext(), data)
|
||||||
|
this.applyControllerContext(nextContext)
|
||||||
|
|
||||||
|
if (nextContext.loadSelectedBout) {
|
||||||
|
this.loadSelectedBoutNumber()
|
||||||
|
}
|
||||||
|
if (nextContext.loadLocalState) {
|
||||||
|
this.loadStateFromLocalStorage()
|
||||||
|
}
|
||||||
|
if (nextContext.resetTimerBanner) {
|
||||||
|
this.resetTimerBannerState()
|
||||||
|
}
|
||||||
|
if (nextContext.unsubscribeMatch) {
|
||||||
|
this.unsubscribeMatchSubscription()
|
||||||
|
}
|
||||||
|
if (nextContext.subscribeMatchId) {
|
||||||
|
this.setupMatchSubscription(nextContext.subscribeMatchId)
|
||||||
|
}
|
||||||
|
if (nextContext.renderNow) {
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMatchPayload(data) {
|
||||||
|
this.applyControllerContext(applyMatchPayloadContext(this.currentContext(), data))
|
||||||
|
}
|
||||||
|
|
||||||
|
storageKey() {
|
||||||
|
return matchStorageKey(this.tournamentIdValue, this.currentBoutNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedBoutStorageKey() {
|
||||||
|
return selectedBoutStorageKeyFromState(this.tournamentIdValue, this.matIdValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStorageChange(event) {
|
||||||
|
const plan = storageChangePlan(this.currentContext(), event.key, this.tournamentIdValue, this.matIdValue)
|
||||||
|
if (plan.loadSelectedBout) this.loadSelectedBoutNumber()
|
||||||
|
if (plan.loadLocalState) this.loadStateFromLocalStorage()
|
||||||
|
if (plan.renderNow) this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSelectedBoutNumber() {
|
||||||
|
const parsedSelection = loadJson(window.localStorage, this.selectedBoutStorageKey())
|
||||||
|
this.currentBoutNumber = selectedBoutNumber(parsedSelection, this.currentQueueBoutNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadStateFromLocalStorage() {
|
||||||
|
const storageKey = this.storageKey()
|
||||||
|
if (!storageKey) {
|
||||||
|
this.state = null
|
||||||
|
this.resetTimerBannerState()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = loadJson(window.localStorage, storageKey)
|
||||||
|
this.applyStatePayload(parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
applyStatePayload(payload) {
|
||||||
|
this.applyControllerContext(applyStatePayloadContext(this.currentContext(), payload))
|
||||||
|
this.updateTimerBannerState()
|
||||||
|
}
|
||||||
|
|
||||||
|
applyControllerContext(context) {
|
||||||
|
this.currentQueueBoutNumber = context.currentQueueBoutNumber
|
||||||
|
this.currentBoutNumber = context.currentBoutNumber
|
||||||
|
this.currentMatchId = context.currentMatchId
|
||||||
|
this.liveMatchData = context.liveMatchData
|
||||||
|
this.lastMatchResult = context.lastMatchResult
|
||||||
|
this.state = context.state
|
||||||
|
this.finished = context.finished
|
||||||
|
this.timerBannerState = context.timerBannerState || null
|
||||||
|
this.previousTimerSnapshot = context.previousTimerSnapshot || {}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentContext() {
|
||||||
|
return {
|
||||||
|
sourceMode: this.sourceModeValue,
|
||||||
|
currentQueueBoutNumber: this.currentQueueBoutNumber,
|
||||||
|
currentBoutNumber: this.currentBoutNumber,
|
||||||
|
currentMatchId: this.currentMatchId,
|
||||||
|
liveMatchData: this.liveMatchData,
|
||||||
|
lastMatchResult: this.lastMatchResult,
|
||||||
|
state: this.state,
|
||||||
|
finished: this.finished,
|
||||||
|
timerBannerState: this.timerBannerState,
|
||||||
|
previousTimerSnapshot: this.previousTimerSnapshot || {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startTicking() {
|
||||||
|
if (this.tickInterval) return
|
||||||
|
this.tickInterval = window.setInterval(() => this.render(), 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.state || !this.state.metadata) {
|
||||||
|
this.renderEmptyState()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.config = getMatchStateConfig(this.state.metadata.ruleset, this.state.metadata.bracketPosition)
|
||||||
|
const viewModel = populatedBoardViewModel(
|
||||||
|
this.config,
|
||||||
|
this.state,
|
||||||
|
this.liveMatchData,
|
||||||
|
this.currentBoutNumber,
|
||||||
|
(seconds) => this.formatClock(seconds)
|
||||||
|
)
|
||||||
|
|
||||||
|
this.applyLiveBoardColors()
|
||||||
|
if (this.hasEmptyStateTarget) this.emptyStateTarget.style.display = "none"
|
||||||
|
this.applyBoardViewModel(viewModel)
|
||||||
|
this.renderTimerBanner()
|
||||||
|
this.renderLastMatchResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEmptyState() {
|
||||||
|
const viewModel = emptyBoardViewModel(this.currentBoutNumber, this.lastMatchResult)
|
||||||
|
this.applyEmptyBoardColors()
|
||||||
|
if (this.hasEmptyStateTarget) this.emptyStateTarget.style.display = "block"
|
||||||
|
this.applyBoardViewModel(viewModel)
|
||||||
|
this.hideTimerBanner()
|
||||||
|
this.renderLastMatchResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
applyBoardViewModel(viewModel) {
|
||||||
|
if (this.hasRedNameTarget) this.redNameTarget.textContent = viewModel.redName
|
||||||
|
if (this.hasRedSchoolTarget) this.redSchoolTarget.textContent = viewModel.redSchool
|
||||||
|
if (this.hasRedScoreTarget) this.redScoreTarget.textContent = viewModel.redScore
|
||||||
|
if (this.hasRedTimerIndicatorTarget) this.redTimerIndicatorTarget.innerHTML = this.renderTimerIndicator(viewModel.redTimerIndicator)
|
||||||
|
if (this.hasGreenNameTarget) this.greenNameTarget.textContent = viewModel.greenName
|
||||||
|
if (this.hasGreenSchoolTarget) this.greenSchoolTarget.textContent = viewModel.greenSchool
|
||||||
|
if (this.hasGreenScoreTarget) this.greenScoreTarget.textContent = viewModel.greenScore
|
||||||
|
if (this.hasGreenTimerIndicatorTarget) this.greenTimerIndicatorTarget.innerHTML = this.renderTimerIndicator(viewModel.greenTimerIndicator)
|
||||||
|
if (this.hasClockTarget) this.clockTarget.textContent = viewModel.clockText
|
||||||
|
if (this.hasPeriodLabelTarget) this.periodLabelTarget.textContent = viewModel.phaseLabel
|
||||||
|
if (this.hasWeightLabelTarget) this.weightLabelTarget.textContent = viewModel.weightLabel
|
||||||
|
if (this.hasBoutLabelTarget) this.boutLabelTarget.textContent = viewModel.boutLabel
|
||||||
|
if (this.hasRedStatsTarget) this.redStatsTarget.textContent = viewModel.redStats
|
||||||
|
if (this.hasGreenStatsTarget) this.greenStatsTarget.textContent = viewModel.greenStats
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLastMatchResult() {
|
||||||
|
if (this.hasLastMatchResultTarget) this.lastMatchResultTarget.textContent = this.lastMatchResult || "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTimerIndicator(label) {
|
||||||
|
if (!label) return ""
|
||||||
|
return `<span class="label label-default">${label}</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
applyLiveBoardColors() {
|
||||||
|
this.applyBoardColors(boardColors(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
applyEmptyBoardColors() {
|
||||||
|
this.applyBoardColors(boardColors(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
applyBoardColors(colors) {
|
||||||
|
if (this.hasRedSectionTarget) this.redSectionTarget.style.background = colors.red
|
||||||
|
if (this.hasCenterSectionTarget) this.centerSectionTarget.style.background = colors.center
|
||||||
|
if (this.hasGreenSectionTarget) this.greenSectionTarget.style.background = colors.green
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimerBannerState() {
|
||||||
|
const nextState = nextTimerBannerState(this.state, this.previousTimerSnapshot)
|
||||||
|
this.timerBannerState = nextState.timerBannerState
|
||||||
|
this.previousTimerSnapshot = nextState.previousTimerSnapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTimerBanner() {
|
||||||
|
if (!this.hasTimerBannerTarget) return
|
||||||
|
const renderState = timerBannerRenderState(
|
||||||
|
this.config,
|
||||||
|
this.state,
|
||||||
|
this.timerBannerState,
|
||||||
|
(seconds) => this.formatClock(seconds)
|
||||||
|
)
|
||||||
|
this.timerBannerState = renderState.timerBannerState
|
||||||
|
|
||||||
|
if (!renderState.visible) {
|
||||||
|
this.hideTimerBanner()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewModel = renderState.viewModel
|
||||||
|
this.timerBannerTarget.style.display = "block"
|
||||||
|
this.timerBannerTarget.style.borderColor = viewModel.color === "green" ? "#1cab2d" : "#c91f1f"
|
||||||
|
if (this.hasTimerBannerLabelTarget) this.timerBannerLabelTarget.textContent = viewModel.label
|
||||||
|
if (this.hasTimerBannerClockTarget) this.timerBannerClockTarget.textContent = viewModel.clockText
|
||||||
|
}
|
||||||
|
|
||||||
|
hideTimerBanner() {
|
||||||
|
if (this.hasTimerBannerTarget) this.timerBannerTarget.style.display = "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
resetTimerBannerState() {
|
||||||
|
this.timerBannerState = null
|
||||||
|
this.previousTimerSnapshot = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
mainClockRunning() {
|
||||||
|
return mainClockRunningFromPresenters(this.config, this.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
formatClock(totalSeconds) {
|
||||||
|
const minutes = Math.floor(totalSeconds / 60)
|
||||||
|
const seconds = totalSeconds % 60
|
||||||
|
return `${minutes}:${seconds.toString().padStart(2, "0")}`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { Controller } from "@hotwired/stimulus"
|
|||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = [
|
static targets = [
|
||||||
"w1Stats", "w2Stats", "winner", "winType",
|
"w1Stats", "w2Stats", "winner", "winType",
|
||||||
"score", "finished", "statusIndicator"
|
"score", "finished", "statusIndicator", "scoreboardContainer"
|
||||||
]
|
]
|
||||||
|
|
||||||
static values = {
|
static values = {
|
||||||
@@ -134,6 +134,9 @@ export default class extends Controller {
|
|||||||
|
|
||||||
if (data.finished !== undefined && this.hasFinishedTarget) {
|
if (data.finished !== undefined && this.hasFinishedTarget) {
|
||||||
this.finishedTarget.textContent = data.finished ? 'Yes' : 'No'
|
this.finishedTarget.textContent = data.finished ? 'Yes' : 'No'
|
||||||
|
if (this.hasScoreboardContainerTarget) {
|
||||||
|
this.scoreboardContainerTarget.style.display = data.finished ? 'none' : 'block'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
803
app/assets/javascripts/controllers/match_state_controller.js
Normal file
803
app/assets/javascripts/controllers/match_state_controller.js
Normal file
@@ -0,0 +1,803 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
import { getMatchStateConfig } from "match-state-config"
|
||||||
|
import {
|
||||||
|
accumulatedMatchSeconds as accumulatedMatchSecondsFromEngine,
|
||||||
|
activeClockForPhase,
|
||||||
|
adjustClockState,
|
||||||
|
applyChoiceAction,
|
||||||
|
applyMatchAction,
|
||||||
|
baseControlForPhase,
|
||||||
|
buildEvent as buildEventFromEngine,
|
||||||
|
buildClockState,
|
||||||
|
buildInitialState,
|
||||||
|
buildTimerState,
|
||||||
|
controlForSelectedPhase,
|
||||||
|
controlFromChoice,
|
||||||
|
currentAuxiliaryTimerSeconds as currentAuxiliaryTimerSecondsFromEngine,
|
||||||
|
currentClockSeconds as currentClockSecondsFromEngine,
|
||||||
|
deleteEventFromState,
|
||||||
|
derivedStats as derivedStatsFromEngine,
|
||||||
|
hasRunningClockOrTimer as hasRunningClockOrTimerFromEngine,
|
||||||
|
matchResultDefaults as matchResultDefaultsFromEngine,
|
||||||
|
moveToNextPhase,
|
||||||
|
moveToPreviousPhase,
|
||||||
|
orderedEvents as orderedEventsFromEngine,
|
||||||
|
opponentParticipant as opponentParticipantFromEngine,
|
||||||
|
phaseIndexForKey as phaseIndexForKeyFromEngine,
|
||||||
|
recomputeDerivedState as recomputeDerivedStateFromEngine,
|
||||||
|
scoreboardStatePayload as scoreboardStatePayloadFromEngine,
|
||||||
|
startAuxiliaryTimerState,
|
||||||
|
startClockState,
|
||||||
|
stopAuxiliaryTimerState,
|
||||||
|
stopClockState,
|
||||||
|
stopAllAuxiliaryTimers as stopAllAuxiliaryTimersFromEngine,
|
||||||
|
swapEventParticipants,
|
||||||
|
swapPhaseParticipants,
|
||||||
|
syncClockSnapshot
|
||||||
|
} from "match-state-engine"
|
||||||
|
import {
|
||||||
|
buildMatchMetadata,
|
||||||
|
buildPersistedState,
|
||||||
|
buildStorageKey,
|
||||||
|
restorePersistedState
|
||||||
|
} from "match-state-serializers"
|
||||||
|
import {
|
||||||
|
loadJson,
|
||||||
|
performIfChanged,
|
||||||
|
removeKey,
|
||||||
|
saveJson
|
||||||
|
} from "match-state-transport"
|
||||||
|
import {
|
||||||
|
choiceViewModel,
|
||||||
|
eventLogSections
|
||||||
|
} from "match-state-presenters"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = [
|
||||||
|
"greenLabel",
|
||||||
|
"redLabel",
|
||||||
|
"greenPanel",
|
||||||
|
"redPanel",
|
||||||
|
"greenName",
|
||||||
|
"redName",
|
||||||
|
"greenSchool",
|
||||||
|
"redSchool",
|
||||||
|
"greenScore",
|
||||||
|
"redScore",
|
||||||
|
"periodLabel",
|
||||||
|
"clock",
|
||||||
|
"clockStatus",
|
||||||
|
"accumulationClock",
|
||||||
|
"matchPosition",
|
||||||
|
"formatName",
|
||||||
|
"choiceActions",
|
||||||
|
"eventLog",
|
||||||
|
"greenControls",
|
||||||
|
"redControls",
|
||||||
|
"matchResultsPanel",
|
||||||
|
"w1StatField",
|
||||||
|
"w2StatField"
|
||||||
|
]
|
||||||
|
|
||||||
|
static values = {
|
||||||
|
matchId: Number,
|
||||||
|
tournamentId: Number,
|
||||||
|
boutNumber: Number,
|
||||||
|
weightLabel: String,
|
||||||
|
bracketPosition: String,
|
||||||
|
ruleset: String,
|
||||||
|
w1Id: Number,
|
||||||
|
w2Id: Number,
|
||||||
|
w1Name: String,
|
||||||
|
w2Name: String,
|
||||||
|
w1School: String,
|
||||||
|
w2School: String
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.config = getMatchStateConfig(this.rulesetValue, this.bracketPositionValue)
|
||||||
|
this.boundHandleClick = this.handleDelegatedClick.bind(this)
|
||||||
|
this.element.addEventListener("click", this.boundHandleClick)
|
||||||
|
this.initializeState()
|
||||||
|
this.loadPersistedState()
|
||||||
|
this.syncClockFromActivePhase()
|
||||||
|
if (this.hasRunningClockOrTimer()) {
|
||||||
|
this.startTicking()
|
||||||
|
}
|
||||||
|
this.render({ rebuildControls: true })
|
||||||
|
this.setupSubscription()
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.element.removeEventListener("click", this.boundHandleClick)
|
||||||
|
window.clearTimeout(this.matchResultsDefaultsTimeout)
|
||||||
|
this.cleanupSubscription()
|
||||||
|
this.saveState()
|
||||||
|
this.stopTicking()
|
||||||
|
this.stopAllAuxiliaryTimers()
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeState() {
|
||||||
|
this.state = this.buildInitialState()
|
||||||
|
}
|
||||||
|
|
||||||
|
buildInitialState() {
|
||||||
|
return buildInitialState(this.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
render(options = {}) {
|
||||||
|
const rebuildControls = options.rebuildControls === true
|
||||||
|
if (this.hasGreenLabelTarget) this.greenLabelTarget.textContent = this.displayLabelForParticipant("w1")
|
||||||
|
if (this.hasRedLabelTarget) this.redLabelTarget.textContent = this.displayLabelForParticipant("w2")
|
||||||
|
if (this.hasGreenPanelTarget) this.applyPanelColor(this.greenPanelTarget, this.colorForParticipant("w1"))
|
||||||
|
if (this.hasRedPanelTarget) this.applyPanelColor(this.redPanelTarget, this.colorForParticipant("w2"))
|
||||||
|
if (this.hasGreenNameTarget) this.greenNameTarget.textContent = this.w1NameValue
|
||||||
|
if (this.hasRedNameTarget) this.redNameTarget.textContent = this.w2NameValue
|
||||||
|
if (this.hasGreenSchoolTarget) this.greenSchoolTarget.textContent = this.w1SchoolValue
|
||||||
|
if (this.hasRedSchoolTarget) this.redSchoolTarget.textContent = this.w2SchoolValue
|
||||||
|
if (this.hasGreenScoreTarget) this.greenScoreTarget.textContent = this.state.participantScores.w1.toString()
|
||||||
|
if (this.hasRedScoreTarget) this.redScoreTarget.textContent = this.state.participantScores.w2.toString()
|
||||||
|
if (this.hasPeriodLabelTarget) this.periodLabelTarget.textContent = this.currentPhase().label
|
||||||
|
this.updateLiveDisplays()
|
||||||
|
if (this.hasMatchPositionTarget) this.matchPositionTarget.textContent = this.humanizePosition(this.state.displayControl)
|
||||||
|
if (this.hasFormatNameTarget) this.formatNameTarget.textContent = this.config.matchFormat.label
|
||||||
|
if (rebuildControls) {
|
||||||
|
if (this.hasGreenControlsTarget) this.greenControlsTarget.innerHTML = this.renderWrestlerControls("w1")
|
||||||
|
if (this.hasRedControlsTarget) this.redControlsTarget.innerHTML = this.renderWrestlerControls("w2")
|
||||||
|
}
|
||||||
|
if (this.hasChoiceActionsTarget) this.choiceActionsTarget.innerHTML = this.renderChoiceActions()
|
||||||
|
if (this.hasEventLogTarget) this.eventLogTarget.innerHTML = this.renderEventLog()
|
||||||
|
this.updateTimerDisplays()
|
||||||
|
this.updateStatFieldsAndBroadcast()
|
||||||
|
this.scheduleApplyMatchResultDefaults()
|
||||||
|
this.saveState()
|
||||||
|
}
|
||||||
|
|
||||||
|
renderWrestlerControls(participantKey) {
|
||||||
|
return Object.values(this.config.wrestler_actions).map((section) => {
|
||||||
|
const content = this.renderWrestlerSection(participantKey, section)
|
||||||
|
if (!content) return ""
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="margin-top: 12px;">
|
||||||
|
<strong>${section.title}</strong>
|
||||||
|
<div class="text-muted" style="margin: 4px 0 8px;">${section.description}</div>
|
||||||
|
<div>${content}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}).join('<hr>')
|
||||||
|
}
|
||||||
|
|
||||||
|
renderWrestlerSection(participantKey, section) {
|
||||||
|
if (!section) return ""
|
||||||
|
|
||||||
|
if (section === this.config.wrestler_actions.timers) {
|
||||||
|
return this.renderTimerSection(participantKey, section)
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionKeys = this.actionKeysForSection(participantKey, section)
|
||||||
|
return this.renderActionButtons(participantKey, actionKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderActionButtons(participantKey, actionKeys) {
|
||||||
|
return actionKeys.map((actionKey) => {
|
||||||
|
const action = this.config.actionsByKey[actionKey]
|
||||||
|
if (!action) return ""
|
||||||
|
|
||||||
|
const buttonClass = this.colorForParticipant(participantKey) === "green" ? "btn-success" : "btn-danger"
|
||||||
|
return `<button type="button" class="btn ${buttonClass} btn-sm" data-match-state-button="score-action" data-participant-key="${participantKey}" data-action-key="${actionKey}">${action.label}</button>`
|
||||||
|
}).join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
actionKeysForSection(participantKey, section) {
|
||||||
|
if (!section?.items) return []
|
||||||
|
|
||||||
|
return section.items.flatMap((itemKey) => {
|
||||||
|
if (itemKey === "global") {
|
||||||
|
return this.availableActionKeysForAvailability(participantKey, "global")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemKey === "position") {
|
||||||
|
const position = this.positionForParticipant(participantKey)
|
||||||
|
return this.availableActionKeysForAvailability(participantKey, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemKey
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
availableActionKeysForAvailability(participantKey, availability) {
|
||||||
|
if (this.currentPhase().type !== "period") return []
|
||||||
|
|
||||||
|
return Object.entries(this.config.actionsByKey)
|
||||||
|
.filter(([, action]) => action.availability === availability)
|
||||||
|
.map(([actionKey]) => actionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTimerSection(participantKey, section) {
|
||||||
|
return (section.items || []).map((timerKey) => {
|
||||||
|
const timerConfig = this.config.timers[timerKey]
|
||||||
|
if (!timerConfig) return ""
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<strong>${timerConfig.label}</strong>: <span data-match-state-timer-display="${participantKey}-${timerKey}">${this.formatClock(this.currentAuxiliaryTimerSeconds(participantKey, timerKey))}</span>
|
||||||
|
<div class="btn-group btn-group-xs" style="margin-left: 8px;">
|
||||||
|
<button type="button" class="btn btn-default" data-match-state-button="timer-action" data-participant-key="${participantKey}" data-timer-key="${timerKey}" data-timer-command="start">Start</button>
|
||||||
|
<button type="button" class="btn btn-default" data-match-state-button="timer-action" data-participant-key="${participantKey}" data-timer-key="${timerKey}" data-timer-command="stop">Stop</button>
|
||||||
|
<button type="button" class="btn btn-default" data-match-state-button="timer-action" data-participant-key="${participantKey}" data-timer-key="${timerKey}" data-timer-command="reset">Reset</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted" data-match-state-timer-status="${participantKey}-${timerKey}">Max ${this.formatClock(timerConfig.maxSeconds)}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}).join("")
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDelegatedClick(event) {
|
||||||
|
const button = event.target.closest("button")
|
||||||
|
if (!button) return
|
||||||
|
|
||||||
|
// Buttons with direct Stimulus actions are handled separately.
|
||||||
|
if (button.dataset.action && button.dataset.action.includes("match-state#")) return
|
||||||
|
|
||||||
|
const buttonType = button.dataset.matchStateButton
|
||||||
|
if (buttonType === "score-action") {
|
||||||
|
this.applyAction(button)
|
||||||
|
} else if (buttonType === "choice-action") {
|
||||||
|
this.applyChoice(button)
|
||||||
|
} else if (buttonType === "timer-action") {
|
||||||
|
this.handleTimerCommand(button)
|
||||||
|
} else if (buttonType === "swap-phase") {
|
||||||
|
this.swapPhase(button)
|
||||||
|
} else if (buttonType === "swap-event") {
|
||||||
|
this.swapEvent(button)
|
||||||
|
} else if (buttonType === "delete-event") {
|
||||||
|
this.deleteEvent(button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyAction(button) {
|
||||||
|
const participantKey = button.dataset.participantKey
|
||||||
|
const actionKey = button.dataset.actionKey
|
||||||
|
if (!applyMatchAction(this.config, this.state, this.currentPhase(), this.currentClockSeconds(), participantKey, actionKey)) return
|
||||||
|
|
||||||
|
this.recomputeDerivedState()
|
||||||
|
this.render({ rebuildControls: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
applyChoice(button) {
|
||||||
|
const phase = this.currentPhase()
|
||||||
|
if (phase.type !== "choice") return
|
||||||
|
|
||||||
|
const participantKey = button.dataset.participantKey
|
||||||
|
const choiceKey = button.dataset.choiceKey
|
||||||
|
|
||||||
|
const result = applyChoiceAction(this.state, phase, this.currentClockSeconds(), participantKey, choiceKey)
|
||||||
|
if (!result.applied) return
|
||||||
|
|
||||||
|
if (result.deferred) {
|
||||||
|
this.recomputeDerivedState()
|
||||||
|
this.render({ rebuildControls: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.advancePhase()
|
||||||
|
}
|
||||||
|
|
||||||
|
swapColors() {
|
||||||
|
this.state.assignment.w1 = this.state.assignment.w1 === "green" ? "red" : "green"
|
||||||
|
this.state.assignment.w2 = this.state.assignment.w2 === "green" ? "red" : "green"
|
||||||
|
this.render({ rebuildControls: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
buildEvent(participantKey, actionKey, options = {}) {
|
||||||
|
return buildEventFromEngine(this.state, this.currentPhase(), this.currentClockSeconds(), participantKey, actionKey, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
startClock() {
|
||||||
|
if (this.currentPhase().type !== "period") return
|
||||||
|
const activeClock = this.activeClock()
|
||||||
|
if (!startClockState(activeClock)) return
|
||||||
|
this.syncClockFromActivePhase()
|
||||||
|
this.startTicking()
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
stopClock() {
|
||||||
|
const activeClock = this.activeClock()
|
||||||
|
if (!stopClockState(activeClock)) return
|
||||||
|
this.syncClockFromActivePhase()
|
||||||
|
this.stopTicking()
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
resetClock() {
|
||||||
|
this.stopClock()
|
||||||
|
const activeClock = this.activeClock()
|
||||||
|
if (!activeClock) return
|
||||||
|
activeClock.remainingSeconds = activeClock.durationSeconds
|
||||||
|
this.syncClockFromActivePhase()
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
addMinute() {
|
||||||
|
this.adjustClock(60)
|
||||||
|
}
|
||||||
|
|
||||||
|
subtractMinute() {
|
||||||
|
this.adjustClock(-60)
|
||||||
|
}
|
||||||
|
|
||||||
|
addSecond() {
|
||||||
|
this.adjustClock(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
subtractSecond() {
|
||||||
|
this.adjustClock(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
previousPhase() {
|
||||||
|
this.stopClock()
|
||||||
|
if (!moveToPreviousPhase(this.config, this.state)) return
|
||||||
|
this.applyPhaseDefaults()
|
||||||
|
this.recomputeDerivedState()
|
||||||
|
this.render({ rebuildControls: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPhase() {
|
||||||
|
this.advancePhase()
|
||||||
|
}
|
||||||
|
|
||||||
|
resetMatch() {
|
||||||
|
const confirmed = window.confirm("Are you sure you want to reset the match? This will wipe the score, reset all timers, and wipe all stats")
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
|
this.stopTicking()
|
||||||
|
this.initializeState()
|
||||||
|
this.syncClockFromActivePhase()
|
||||||
|
this.clearPersistedState()
|
||||||
|
this.render({ rebuildControls: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
advancePhase() {
|
||||||
|
this.stopClock()
|
||||||
|
if (!moveToNextPhase(this.config, this.state)) return
|
||||||
|
this.applyPhaseDefaults()
|
||||||
|
this.recomputeDerivedState()
|
||||||
|
this.render({ rebuildControls: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteEvent(button) {
|
||||||
|
const eventId = Number(button.dataset.eventId)
|
||||||
|
if (!deleteEventFromState(this.config, this.state, eventId)) return
|
||||||
|
this.recomputeDerivedState()
|
||||||
|
this.render({ rebuildControls: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
swapEvent(button) {
|
||||||
|
const eventId = Number(button.dataset.eventId)
|
||||||
|
if (!swapEventParticipants(this.config, this.state, eventId)) return
|
||||||
|
this.recomputeDerivedState()
|
||||||
|
this.render({ rebuildControls: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
swapPhase(button) {
|
||||||
|
const phaseKey = button.dataset.phaseKey
|
||||||
|
if (!swapPhaseParticipants(this.config, this.state, phaseKey)) return
|
||||||
|
this.recomputeDerivedState()
|
||||||
|
this.render({ rebuildControls: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTimerCommand(button) {
|
||||||
|
const participantKey = button.dataset.participantKey
|
||||||
|
const timerKey = button.dataset.timerKey
|
||||||
|
const command = button.dataset.timerCommand
|
||||||
|
|
||||||
|
if (command === "start") this.startAuxiliaryTimer(participantKey, timerKey)
|
||||||
|
if (command === "stop") this.stopAuxiliaryTimer(participantKey, timerKey)
|
||||||
|
if (command === "reset") this.resetAuxiliaryTimer(participantKey, timerKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
startAuxiliaryTimer(participantKey, timerKey) {
|
||||||
|
const timer = this.state.timers[participantKey][timerKey]
|
||||||
|
if (!startAuxiliaryTimerState(timer)) return
|
||||||
|
this.startTicking()
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAuxiliaryTimer(participantKey, timerKey) {
|
||||||
|
const timer = this.state.timers[participantKey][timerKey]
|
||||||
|
const { stopped, elapsedSeconds } = stopAuxiliaryTimerState(timer)
|
||||||
|
if (!stopped) return
|
||||||
|
|
||||||
|
if (elapsedSeconds > 0) {
|
||||||
|
this.state.events.push({
|
||||||
|
...this.buildEvent(participantKey, `timer_used_${timerKey}`),
|
||||||
|
elapsedSeconds: elapsedSeconds
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
resetAuxiliaryTimer(participantKey, timerKey) {
|
||||||
|
this.stopAuxiliaryTimer(participantKey, timerKey)
|
||||||
|
const timer = this.state.timers[participantKey][timerKey]
|
||||||
|
timer.remainingSeconds = this.config.timers[timerKey].maxSeconds
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTimerState() {
|
||||||
|
return buildTimerState(this.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildClockState() {
|
||||||
|
return buildClockState(this.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentClockSeconds() {
|
||||||
|
return currentClockSecondsFromEngine(this.activeClock())
|
||||||
|
}
|
||||||
|
|
||||||
|
currentAuxiliaryTimerSeconds(participantKey, timerKey) {
|
||||||
|
return currentAuxiliaryTimerSecondsFromEngine(this.state.timers[participantKey][timerKey])
|
||||||
|
}
|
||||||
|
|
||||||
|
hasRunningClockOrTimer() {
|
||||||
|
return hasRunningClockOrTimerFromEngine(this.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
startTicking() {
|
||||||
|
if (this.tickInterval) return
|
||||||
|
this.tickInterval = window.setInterval(() => {
|
||||||
|
if (this.activeClock()?.running && this.currentClockSeconds() === 0) {
|
||||||
|
this.stopClock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const participantKey of ["w1", "w2"]) {
|
||||||
|
for (const timerKey of Object.keys(this.state.timers[participantKey])) {
|
||||||
|
if (this.state.timers[participantKey][timerKey].running && this.currentAuxiliaryTimerSeconds(participantKey, timerKey) === 0) {
|
||||||
|
this.stopAuxiliaryTimer(participantKey, timerKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateLiveDisplays()
|
||||||
|
this.updateTimerDisplays()
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
stopTicking() {
|
||||||
|
if (!this.tickInterval) return
|
||||||
|
window.clearInterval(this.tickInterval)
|
||||||
|
this.tickInterval = null
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAllAuxiliaryTimers() {
|
||||||
|
stopAllAuxiliaryTimersFromEngine(this.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
positionForParticipant(participantKey) {
|
||||||
|
if (this.state.displayControl === "neutral") return "neutral"
|
||||||
|
if (this.state.displayControl === `${participantKey}_control`) return "top"
|
||||||
|
return "bottom"
|
||||||
|
}
|
||||||
|
|
||||||
|
opponentParticipant(participantKey) {
|
||||||
|
return opponentParticipantFromEngine(participantKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
humanizePosition(position) {
|
||||||
|
if (position === "neutral") return "Neutral"
|
||||||
|
if (position === "green_control") return "Green In Control"
|
||||||
|
if (position === "red_control") return "Red In Control"
|
||||||
|
return position
|
||||||
|
}
|
||||||
|
|
||||||
|
recomputeDerivedState() {
|
||||||
|
recomputeDerivedStateFromEngine(this.config, this.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEventLog() {
|
||||||
|
if (this.state.events.length === 0) {
|
||||||
|
return '<p class="text-muted">No events yet.</p>'
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventLogSections(this.config, this.state, (seconds) => this.formatClock(seconds)).map((section) => {
|
||||||
|
const items = section.items.map((eventRecord) => {
|
||||||
|
return `
|
||||||
|
<div class="well well-sm" style="margin-bottom: 8px;">
|
||||||
|
<div style="display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px;">
|
||||||
|
<div style="flex: 1 1 260px; min-width: 0; overflow-wrap: anywhere;">
|
||||||
|
<strong>${eventRecord.colorLabel}</strong> ${eventRecord.actionLabel}
|
||||||
|
<span class="text-muted">(${eventRecord.clockLabel})</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; flex-wrap: wrap; gap: 8px; flex: 0 0 auto;">
|
||||||
|
<button type="button" class="btn btn-xs btn-link" data-match-state-button="swap-event" data-event-id="${eventRecord.id}">Swap</button>
|
||||||
|
<button type="button" class="btn btn-xs btn-link" data-match-state-button="delete-event" data-event-id="${eventRecord.id}">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}).join("")
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="margin-bottom: 16px;">
|
||||||
|
<div style="display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px;">
|
||||||
|
<h5 style="margin: 0;">${section.label}</h5>
|
||||||
|
<button type="button" class="btn btn-xs btn-link" data-match-state-button="swap-phase" data-phase-key="${section.key}">Swap Entire Period</button>
|
||||||
|
</div>
|
||||||
|
${items}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}).join("")
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLiveDisplays() {
|
||||||
|
if (this.hasClockTarget) {
|
||||||
|
this.clockTarget.textContent = this.currentPhase().type === "period" ? this.formatClock(this.currentClockSeconds()) : "-"
|
||||||
|
}
|
||||||
|
if (this.hasClockStatusTarget) {
|
||||||
|
this.clockStatusTarget.textContent = this.currentPhase().type === "period"
|
||||||
|
? (this.activeClock()?.running ? "Running" : "Stopped")
|
||||||
|
: "Choice"
|
||||||
|
}
|
||||||
|
if (this.hasAccumulationClockTarget) {
|
||||||
|
this.accumulationClockTarget.textContent = this.formatClock(this.accumulatedMatchSeconds())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimerDisplays() {
|
||||||
|
for (const participantKey of ["w1", "w2"]) {
|
||||||
|
for (const [timerKey, timerConfig] of Object.entries(this.config.timers)) {
|
||||||
|
const display = this.element.querySelector(`[data-match-state-timer-display="${participantKey}-${timerKey}"]`)
|
||||||
|
const status = this.element.querySelector(`[data-match-state-timer-status="${participantKey}-${timerKey}"]`)
|
||||||
|
if (display) {
|
||||||
|
display.textContent = this.formatClock(this.currentAuxiliaryTimerSeconds(participantKey, timerKey))
|
||||||
|
}
|
||||||
|
if (status) {
|
||||||
|
const running = this.state.timers[participantKey][timerKey].running
|
||||||
|
status.textContent = `Max ${this.formatClock(timerConfig.maxSeconds)}${running ? " • running" : ""}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChoiceActions() {
|
||||||
|
const phase = this.currentPhase()
|
||||||
|
const viewModel = choiceViewModel(this.config, this.state, phase, {
|
||||||
|
w1: { name: this.w1NameValue },
|
||||||
|
w2: { name: this.w2NameValue }
|
||||||
|
})
|
||||||
|
if (!viewModel) return ""
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="well well-sm">
|
||||||
|
<div><strong>${viewModel.label}</strong></div>
|
||||||
|
<div class="text-muted" style="margin: 6px 0;">${viewModel.selectionText}</div>
|
||||||
|
<div>${viewModel.buttons.map((button) => `
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn ${button.buttonClass} btn-sm"
|
||||||
|
data-match-state-button="choice-action"
|
||||||
|
data-participant-key="${button.participantKey}"
|
||||||
|
data-choice-key="${button.choiceKey}">
|
||||||
|
${button.text}
|
||||||
|
</button>
|
||||||
|
`).join(" ")}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPhase() {
|
||||||
|
return this.config.phaseSequence[this.state.phaseIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
applyPhaseDefaults() {
|
||||||
|
this.syncClockFromActivePhase()
|
||||||
|
this.state.control = this.baseControlForCurrentPhase()
|
||||||
|
}
|
||||||
|
|
||||||
|
baseControlForCurrentPhase() {
|
||||||
|
return baseControlForPhase(this.currentPhase(), this.state.selections, this.state.control)
|
||||||
|
}
|
||||||
|
|
||||||
|
controlFromChoice(selection) {
|
||||||
|
return controlFromChoice(selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
colorForParticipant(participantKey) {
|
||||||
|
return this.state.assignment[participantKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
displayLabelForParticipant(participantKey) {
|
||||||
|
return this.colorForParticipant(participantKey) === "green" ? "Green" : "Red"
|
||||||
|
}
|
||||||
|
|
||||||
|
applyPanelColor(panelElement, color) {
|
||||||
|
panelElement.classList.remove("panel-success", "panel-danger")
|
||||||
|
panelElement.classList.add(color === "green" ? "panel-success" : "panel-danger")
|
||||||
|
}
|
||||||
|
|
||||||
|
controlForSelectedPhase() {
|
||||||
|
return controlForSelectedPhase(this.config, this.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseControlForPhase(phase) {
|
||||||
|
return baseControlForPhase(phase, this.state.selections, this.state.control)
|
||||||
|
}
|
||||||
|
|
||||||
|
orderedEvents() {
|
||||||
|
return orderedEventsFromEngine(this.config, this.state.events)
|
||||||
|
}
|
||||||
|
|
||||||
|
phaseIndexForKey(phaseKey) {
|
||||||
|
return phaseIndexForKeyFromEngine(this.config, phaseKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
activeClock() {
|
||||||
|
return activeClockForPhase(this.state, this.currentPhase())
|
||||||
|
}
|
||||||
|
|
||||||
|
setupSubscription() {
|
||||||
|
this.cleanupSubscription()
|
||||||
|
if (!this.matchIdValue || !window.App || !window.App.cable) return
|
||||||
|
|
||||||
|
this.matchSubscription = App.cable.subscriptions.create(
|
||||||
|
{
|
||||||
|
channel: "MatchChannel",
|
||||||
|
match_id: this.matchIdValue
|
||||||
|
},
|
||||||
|
{
|
||||||
|
connected: () => {
|
||||||
|
this.isConnected = true
|
||||||
|
this.pushDerivedStatsToChannel()
|
||||||
|
this.pushScoreboardStateToChannel()
|
||||||
|
},
|
||||||
|
disconnected: () => {
|
||||||
|
this.isConnected = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupSubscription() {
|
||||||
|
if (!this.matchSubscription) return
|
||||||
|
try {
|
||||||
|
this.matchSubscription.unsubscribe()
|
||||||
|
} catch (_error) {
|
||||||
|
}
|
||||||
|
this.matchSubscription = null
|
||||||
|
this.isConnected = false
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatFieldsAndBroadcast() {
|
||||||
|
const derivedStats = this.derivedStats()
|
||||||
|
|
||||||
|
if (this.hasW1StatFieldTarget) this.w1StatFieldTarget.value = derivedStats.w1
|
||||||
|
if (this.hasW2StatFieldTarget) this.w2StatFieldTarget.value = derivedStats.w2
|
||||||
|
|
||||||
|
this.lastDerivedStats = derivedStats
|
||||||
|
this.pushDerivedStatsToChannel()
|
||||||
|
this.pushScoreboardStateToChannel()
|
||||||
|
}
|
||||||
|
|
||||||
|
pushDerivedStatsToChannel() {
|
||||||
|
if (!this.matchSubscription || !this.lastDerivedStats) return
|
||||||
|
this.lastBroadcastStats = performIfChanged(this.matchSubscription, "send_stat", {
|
||||||
|
new_w1_stat: this.lastDerivedStats.w1,
|
||||||
|
new_w2_stat: this.lastDerivedStats.w2
|
||||||
|
}, this.lastBroadcastStats)
|
||||||
|
}
|
||||||
|
|
||||||
|
pushScoreboardStateToChannel() {
|
||||||
|
if (!this.matchSubscription) return
|
||||||
|
|
||||||
|
this.lastBroadcastScoreboardState = performIfChanged(this.matchSubscription, "send_scoreboard", {
|
||||||
|
scoreboard_state: this.scoreboardStatePayload()
|
||||||
|
}, this.lastBroadcastScoreboardState)
|
||||||
|
}
|
||||||
|
|
||||||
|
applyMatchResultDefaults() {
|
||||||
|
const controllerElement = this.matchResultsPanelTarget?.querySelector('[data-controller~="match-score"]')
|
||||||
|
if (!controllerElement) return
|
||||||
|
|
||||||
|
const scoreController = this.application.getControllerForElementAndIdentifier(controllerElement, "match-score")
|
||||||
|
if (!scoreController || typeof scoreController.applyDefaultResults !== "function") return
|
||||||
|
|
||||||
|
scoreController.applyDefaultResults(
|
||||||
|
matchResultDefaultsFromEngine(this.state, {
|
||||||
|
w1Id: this.w1IdValue,
|
||||||
|
w2Id: this.w2IdValue,
|
||||||
|
currentPhase: this.currentPhase(),
|
||||||
|
accumulationSeconds: this.accumulatedMatchSeconds()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleApplyMatchResultDefaults() {
|
||||||
|
if (!this.hasMatchResultsPanelTarget) return
|
||||||
|
|
||||||
|
window.clearTimeout(this.matchResultsDefaultsTimeout)
|
||||||
|
this.matchResultsDefaultsTimeout = window.setTimeout(() => {
|
||||||
|
this.applyMatchResultDefaults()
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
storageKey() {
|
||||||
|
return buildStorageKey(this.tournamentIdValue, this.boutNumberValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPersistedState() {
|
||||||
|
const parsedState = loadJson(window.localStorage, this.storageKey())
|
||||||
|
if (!parsedState) {
|
||||||
|
if (window.localStorage.getItem(this.storageKey())) {
|
||||||
|
this.clearPersistedState()
|
||||||
|
this.state = this.buildInitialState()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.state = restorePersistedState(this.config, parsedState)
|
||||||
|
} catch (_error) {
|
||||||
|
this.clearPersistedState()
|
||||||
|
this.state = this.buildInitialState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveState() {
|
||||||
|
const persistedState = buildPersistedState(this.state, this.matchMetadata())
|
||||||
|
saveJson(window.localStorage, this.storageKey(), persistedState)
|
||||||
|
}
|
||||||
|
|
||||||
|
clearPersistedState() {
|
||||||
|
removeKey(window.localStorage, this.storageKey())
|
||||||
|
}
|
||||||
|
|
||||||
|
accumulatedMatchSeconds() {
|
||||||
|
return accumulatedMatchSecondsFromEngine(this.config, this.state, this.currentPhase().key)
|
||||||
|
}
|
||||||
|
|
||||||
|
syncClockFromActivePhase() {
|
||||||
|
this.state.clock = syncClockSnapshot(this.activeClock())
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustClock(deltaSeconds) {
|
||||||
|
if (this.currentPhase().type !== "period") return
|
||||||
|
|
||||||
|
const activeClock = this.activeClock()
|
||||||
|
if (!adjustClockState(activeClock, deltaSeconds)) return
|
||||||
|
this.syncClockFromActivePhase()
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
formatClock(totalSeconds) {
|
||||||
|
const minutes = Math.floor(totalSeconds / 60)
|
||||||
|
const seconds = totalSeconds % 60
|
||||||
|
return `${minutes}:${seconds.toString().padStart(2, "0")}`
|
||||||
|
}
|
||||||
|
|
||||||
|
derivedStats() {
|
||||||
|
return derivedStatsFromEngine(this.config, this.state.events)
|
||||||
|
}
|
||||||
|
|
||||||
|
scoreboardStatePayload() {
|
||||||
|
return scoreboardStatePayloadFromEngine(this.config, this.state, this.matchMetadata())
|
||||||
|
}
|
||||||
|
|
||||||
|
matchMetadata() {
|
||||||
|
return buildMatchMetadata({
|
||||||
|
tournamentId: this.tournamentIdValue,
|
||||||
|
boutNumber: this.boutNumberValue,
|
||||||
|
weightLabel: this.weightLabelValue,
|
||||||
|
ruleset: this.rulesetValue,
|
||||||
|
bracketPosition: this.bracketPositionValue,
|
||||||
|
w1Name: this.w1NameValue,
|
||||||
|
w2Name: this.w2NameValue,
|
||||||
|
w1School: this.w1SchoolValue,
|
||||||
|
w2School: this.w2SchoolValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["stream", "statusIndicator"]
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.setupSubscription()
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.cleanupSubscription()
|
||||||
|
}
|
||||||
|
|
||||||
|
setupSubscription() {
|
||||||
|
this.cleanupSubscription()
|
||||||
|
this.setStatus("Connecting to server for real-time bout board updates...", "info")
|
||||||
|
|
||||||
|
if (!this.hasStreamTarget) {
|
||||||
|
this.setStatus("Error: Stream source not found.", "danger")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const signedStreamName = this.streamTarget.getAttribute("signed-stream-name")
|
||||||
|
if (!signedStreamName) {
|
||||||
|
this.setStatus("Error: Invalid stream source.", "danger")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.App || !window.App.cable) {
|
||||||
|
this.setStatus("Error: WebSockets unavailable. Bout board won't update in real-time. Refresh the page to update.", "danger")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subscription = App.cable.subscriptions.create(
|
||||||
|
{
|
||||||
|
channel: "Turbo::StreamsChannel",
|
||||||
|
signed_stream_name: signedStreamName
|
||||||
|
},
|
||||||
|
{
|
||||||
|
connected: () => {
|
||||||
|
this.setStatus("Connected: Bout board updating in real-time.", "success")
|
||||||
|
},
|
||||||
|
disconnected: () => {
|
||||||
|
this.setStatus("Disconnected: Live bout board updates paused.", "warning")
|
||||||
|
},
|
||||||
|
rejected: () => {
|
||||||
|
this.setStatus("Error: Live bout board connection rejected.", "danger")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupSubscription() {
|
||||||
|
if (!this.subscription) return
|
||||||
|
this.subscription.unsubscribe()
|
||||||
|
this.subscription = null
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus(message, type) {
|
||||||
|
if (!this.hasStatusIndicatorTarget) return
|
||||||
|
|
||||||
|
this.statusIndicatorTarget.innerText = message
|
||||||
|
this.statusIndicatorTarget.classList.remove("alert-secondary", "alert-info", "alert-success", "alert-warning", "alert-danger")
|
||||||
|
|
||||||
|
if (type === "success") this.statusIndicatorTarget.classList.add("alert-success")
|
||||||
|
else if (type === "warning") this.statusIndicatorTarget.classList.add("alert-warning")
|
||||||
|
else if (type === "danger") this.statusIndicatorTarget.classList.add("alert-danger")
|
||||||
|
else this.statusIndicatorTarget.classList.add("alert-info")
|
||||||
|
}
|
||||||
|
}
|
||||||
344
app/assets/javascripts/lib/match_state/config.js
Normal file
344
app/assets/javascripts/lib/match_state/config.js
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
/*
|
||||||
|
State page config contract
|
||||||
|
==========================
|
||||||
|
|
||||||
|
The state page responds to these top-level config objects:
|
||||||
|
|
||||||
|
1. `wrestler_actions`
|
||||||
|
Drives the wrestler-side UI from top to bottom inside each wrestler panel.
|
||||||
|
The controller renders these sections in order, so the order in this object
|
||||||
|
controls the visual order underneath each wrestler's name, school, and score.
|
||||||
|
Supported sections:
|
||||||
|
- `match_actions`
|
||||||
|
- `timers`
|
||||||
|
- `extra_actions`
|
||||||
|
|
||||||
|
Each section may define:
|
||||||
|
- `title`
|
||||||
|
- `description`
|
||||||
|
- `items`
|
||||||
|
|
||||||
|
How the state page uses it:
|
||||||
|
- `match_actions.items`
|
||||||
|
Each item is either:
|
||||||
|
- a literal action key, or
|
||||||
|
- a special alias such as `global` or `position`
|
||||||
|
The state page expands those aliases into the currently legal actions for
|
||||||
|
that wrestler and renders them as buttons.
|
||||||
|
- `timers.items`
|
||||||
|
Each item is a timer key. The state page renders the timer display plus
|
||||||
|
start/stop/reset buttons for each listed timer.
|
||||||
|
- `extra_actions.items`
|
||||||
|
Each item is a literal action key rendered as an always-visible button
|
||||||
|
underneath the timer section.
|
||||||
|
|
||||||
|
2. `actionsByKey`
|
||||||
|
Canonical definitions for match actions and history actions.
|
||||||
|
This is the source of truth for how a button behaves and how an action
|
||||||
|
should appear in the event log.
|
||||||
|
Each action may define:
|
||||||
|
- `label`
|
||||||
|
- `availability`
|
||||||
|
- `statCode`
|
||||||
|
- `effect`
|
||||||
|
- `progression`
|
||||||
|
|
||||||
|
How the state page uses it:
|
||||||
|
- `label`
|
||||||
|
Used for button text and event log text.
|
||||||
|
- `availability`
|
||||||
|
Used when `wrestler_actions.match_actions.items` includes aliases like
|
||||||
|
`global` or `position`.
|
||||||
|
- `effect`
|
||||||
|
Used by the rules engine to update score and match position when replaying
|
||||||
|
the event list.
|
||||||
|
- `statCode`
|
||||||
|
Used when rewriting the hidden `w1_stat` / `w2_stat` fields from the
|
||||||
|
structured event log for websocket sync and match submission.
|
||||||
|
- `progression`
|
||||||
|
Used for progressive actions like stalling, caution, and penalty to decide
|
||||||
|
if the opponent should automatically receive a linked point-scoring event.
|
||||||
|
|
||||||
|
Supported `availability` values used by the wrestler-side UI:
|
||||||
|
- `global`
|
||||||
|
- `neutral`
|
||||||
|
- `top`
|
||||||
|
- `bottom`
|
||||||
|
- `extra`
|
||||||
|
|
||||||
|
3. `timers`
|
||||||
|
Canonical timer definitions keyed by timer name.
|
||||||
|
This controls both the timer controls in the wrestler panel and how timer
|
||||||
|
usage is labeled in the event log.
|
||||||
|
|
||||||
|
How the state page uses it:
|
||||||
|
- `label`
|
||||||
|
Displayed next to the running timer value in the wrestler panel.
|
||||||
|
- `maxSeconds`
|
||||||
|
Used to initialize, reset, clamp, and render the timer.
|
||||||
|
- `historyLabel`
|
||||||
|
Used when a timer stop event is recorded in history.
|
||||||
|
- `statCode`
|
||||||
|
Used when rewriting the hidden `w1_stat` / `w2_stat` fields for timer-used
|
||||||
|
events.
|
||||||
|
|
||||||
|
4. `phases`
|
||||||
|
Defines the period / choice sequence for this wrestling style.
|
||||||
|
The active phase drives:
|
||||||
|
- the main match clock
|
||||||
|
- phase labels
|
||||||
|
- start-of-period position behavior
|
||||||
|
- choice button behavior
|
||||||
|
- event grouping in the history list
|
||||||
|
|
||||||
|
How the state page uses it:
|
||||||
|
- chooses which phase sequence to use from bracket position
|
||||||
|
- builds the main match clock state for timed phases
|
||||||
|
- determines whether the current phase is a period or a choice phase
|
||||||
|
- determines how a period starts (`neutral` or from a prior choice)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const RULESETS = {
|
||||||
|
folkstyle_usa: {
|
||||||
|
id: "folkstyle_usa",
|
||||||
|
|
||||||
|
wrestler_actions: {
|
||||||
|
match_actions: {
|
||||||
|
title: "Match Actions",
|
||||||
|
description: "Scoring and match-state actions available based on current position.",
|
||||||
|
items: ["global", "position"]
|
||||||
|
},
|
||||||
|
timers: {
|
||||||
|
title: "Wrestler Timers",
|
||||||
|
description: "Track blood, injury, recovery, and head/neck time for this wrestler.",
|
||||||
|
items: ["blood", "injury", "recovery", "head_neck"]
|
||||||
|
},
|
||||||
|
extra_actions: {
|
||||||
|
title: "Extra Actions",
|
||||||
|
description: "Force the match into a specific position and record it in history.",
|
||||||
|
items: ["position_neutral", "position_top", "position_bottom"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actionsByKey: {
|
||||||
|
stalling: {
|
||||||
|
label: "Stalling",
|
||||||
|
availability: "global",
|
||||||
|
statCode: "S",
|
||||||
|
effect: { points: 0 },
|
||||||
|
progression: [0, 1, 1, 2]
|
||||||
|
},
|
||||||
|
caution: {
|
||||||
|
label: "Caution",
|
||||||
|
availability: "global",
|
||||||
|
statCode: "C",
|
||||||
|
effect: { points: 0 },
|
||||||
|
progression: [0, 0, 1]
|
||||||
|
},
|
||||||
|
penalty: {
|
||||||
|
label: "Penalty",
|
||||||
|
availability: "global",
|
||||||
|
statCode: "P",
|
||||||
|
effect: { points: 0 },
|
||||||
|
progression: [1, 1, 2]
|
||||||
|
},
|
||||||
|
minus_1: {
|
||||||
|
label: "-1 Point",
|
||||||
|
availability: "global",
|
||||||
|
statCode: "-1",
|
||||||
|
effect: { points: -1 }
|
||||||
|
},
|
||||||
|
plus_1: {
|
||||||
|
label: "+1 Point",
|
||||||
|
availability: "global",
|
||||||
|
statCode: "+1",
|
||||||
|
effect: { points: 1 }
|
||||||
|
},
|
||||||
|
plus_2: {
|
||||||
|
label: "+2 Points",
|
||||||
|
statCode: "+2",
|
||||||
|
effect: { points: 2 }
|
||||||
|
},
|
||||||
|
takedown_3: {
|
||||||
|
label: "Takedown +3",
|
||||||
|
availability: "neutral",
|
||||||
|
statCode: "T3",
|
||||||
|
effect: { points: 3, nextPosition: "top" }
|
||||||
|
},
|
||||||
|
nearfall_2: {
|
||||||
|
label: "Nearfall +2",
|
||||||
|
availability: "top",
|
||||||
|
statCode: "N2",
|
||||||
|
effect: { points: 2 }
|
||||||
|
},
|
||||||
|
nearfall_3: {
|
||||||
|
label: "Nearfall +3",
|
||||||
|
availability: "top",
|
||||||
|
statCode: "N3",
|
||||||
|
effect: { points: 3 }
|
||||||
|
},
|
||||||
|
nearfall_4: {
|
||||||
|
label: "Nearfall +4",
|
||||||
|
availability: "top",
|
||||||
|
statCode: "N4",
|
||||||
|
effect: { points: 4 }
|
||||||
|
},
|
||||||
|
nearfall_5: {
|
||||||
|
label: "Nearfall +5",
|
||||||
|
availability: "top",
|
||||||
|
statCode: "N5",
|
||||||
|
effect: { points: 5 }
|
||||||
|
},
|
||||||
|
escape_1: {
|
||||||
|
label: "Escape +1",
|
||||||
|
availability: "bottom",
|
||||||
|
statCode: "E1",
|
||||||
|
effect: { points: 1, nextPosition: "neutral" }
|
||||||
|
},
|
||||||
|
reversal_2: {
|
||||||
|
label: "Reversal +2",
|
||||||
|
availability: "bottom",
|
||||||
|
statCode: "R2",
|
||||||
|
effect: { points: 2, nextPosition: "top" }
|
||||||
|
},
|
||||||
|
position_neutral: {
|
||||||
|
label: "Neutral",
|
||||||
|
availability: "extra",
|
||||||
|
statCode: "|Neutral|",
|
||||||
|
effect: { points: 0, nextPosition: "neutral" }
|
||||||
|
},
|
||||||
|
position_top: {
|
||||||
|
label: "Top",
|
||||||
|
availability: "extra",
|
||||||
|
statCode: "|Top|",
|
||||||
|
effect: { points: 0, nextPosition: "top" }
|
||||||
|
},
|
||||||
|
position_bottom: {
|
||||||
|
label: "Bottom",
|
||||||
|
availability: "extra",
|
||||||
|
statCode: "|Bottom|",
|
||||||
|
effect: { points: 0, nextPosition: "bottom" }
|
||||||
|
},
|
||||||
|
choice_top: {
|
||||||
|
label: "Choice: Top",
|
||||||
|
statCode: "|Chose Top|"
|
||||||
|
},
|
||||||
|
choice_bottom: {
|
||||||
|
label: "Choice: Bottom",
|
||||||
|
statCode: "|Chose Bottom|"
|
||||||
|
},
|
||||||
|
choice_neutral: {
|
||||||
|
label: "Choice: Neutral",
|
||||||
|
statCode: "|Chose Neutral|"
|
||||||
|
},
|
||||||
|
choice_defer: {
|
||||||
|
label: "Choice: Defer",
|
||||||
|
statCode: "|Deferred|"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
timers: {
|
||||||
|
blood: { maxSeconds: 300, label: "Blood", historyLabel: "Blood Time Used", statCode: "Blood Time" },
|
||||||
|
injury: { maxSeconds: 90, label: "Injury", historyLabel: "Injury Time Used", statCode: "Injury Time" },
|
||||||
|
recovery: { maxSeconds: 120, label: "Recovery", historyLabel: "Recovery Time Used", statCode: "Recovery Time" },
|
||||||
|
head_neck: { maxSeconds: 300, label: "Head/Neck", historyLabel: "Head/Neck Time Used", statCode: "Head/Neck Time" }
|
||||||
|
},
|
||||||
|
|
||||||
|
phases: {
|
||||||
|
championship: {
|
||||||
|
label: "Championship Format",
|
||||||
|
sequence: [
|
||||||
|
{ key: "period_1", label: "Period 1", type: "period", startsIn: "neutral", clockSeconds: 120 },
|
||||||
|
{ key: "choice_1", label: "Choice 1", type: "choice", chooser: "either", options: ["top", "bottom", "neutral", "defer"] },
|
||||||
|
{ key: "period_2", label: "Period 2", type: "period", startsFromChoice: "choice_1", clockSeconds: 120 },
|
||||||
|
{ key: "choice_2", label: "Choice 2", type: "choice", chooser: "other", options: ["top", "bottom", "neutral"] },
|
||||||
|
{ key: "period_3", label: "Period 3", type: "period", startsFromChoice: "choice_2", clockSeconds: 120 },
|
||||||
|
{ key: "sv_1", label: "SV-1", type: "period", startsIn: "neutral", clockSeconds: 60, overtimeType: "SV-1" },
|
||||||
|
{ key: "choice_3", label: "Choice 3", type: "choice", chooser: "either", options: ["top", "bottom", "defer"] },
|
||||||
|
{ key: "tb_1a", label: "TB-1A", type: "period", startsFromChoice: "choice_3", clockSeconds: 30, overtimeType: "TB-1" },
|
||||||
|
{ key: "choice_4", label: "Choice 4", type: "choice", chooser: "other", options: ["top", "bottom"] },
|
||||||
|
{ key: "tb_1b", label: "TB-1B", type: "period", startsFromChoice: "choice_4", clockSeconds: 30, overtimeType: "TB-1" },
|
||||||
|
{ key: "choice_5", label: "Choice 5", type: "choice", chooser: "either", options: ["top", "bottom"] },
|
||||||
|
{ key: "utb", label: "UTB", type: "period", startsFromChoice: "choice_5", clockSeconds: 30, overtimeType: "UTB" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
consolation: {
|
||||||
|
label: "Consolation Format",
|
||||||
|
sequence: [
|
||||||
|
{ key: "period_1", label: "Period 1", type: "period", startsIn: "neutral", clockSeconds: 60 },
|
||||||
|
{ key: "choice_1", label: "Choice 1", type: "choice", chooser: "either", options: ["top", "bottom", "neutral", "defer"] },
|
||||||
|
{ key: "period_2", label: "Period 2", type: "period", startsFromChoice: "choice_1", clockSeconds: 120 },
|
||||||
|
{ key: "choice_2", label: "Choice 2", type: "choice", chooser: "other", options: ["top", "bottom", "neutral"] },
|
||||||
|
{ key: "period_3", label: "Period 3", type: "period", startsFromChoice: "choice_2", clockSeconds: 120 },
|
||||||
|
{ key: "sv_1", label: "SV-1", type: "period", startsIn: "neutral", clockSeconds: 60, overtimeType: "SV-1" },
|
||||||
|
{ key: "choice_3", label: "Choice 3", type: "choice", chooser: "either", options: ["top", "bottom", "defer"] },
|
||||||
|
{ key: "tb_1a", label: "TB-1A", type: "period", startsFromChoice: "choice_3", clockSeconds: 30, overtimeType: "TB-1" },
|
||||||
|
{ key: "choice_4", label: "Choice 4", type: "choice", chooser: "other", options: ["top", "bottom"] },
|
||||||
|
{ key: "tb_1b", label: "TB-1B", type: "period", startsFromChoice: "choice_4", clockSeconds: 30, overtimeType: "TB-1" },
|
||||||
|
{ key: "choice_5", label: "Choice 5", type: "choice", chooser: "either", options: ["top", "bottom"] },
|
||||||
|
{ key: "utb", label: "UTB", type: "period", startsFromChoice: "choice_5", clockSeconds: 30, overtimeType: "UTB" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function phaseStyleKeyForBracketPosition(bracketPosition) {
|
||||||
|
if (!bracketPosition) return "championship"
|
||||||
|
|
||||||
|
if (
|
||||||
|
bracketPosition.includes("Conso") ||
|
||||||
|
["3/4", "5/6", "7/8"].includes(bracketPosition)
|
||||||
|
) {
|
||||||
|
return "consolation"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "championship"
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildActionEffects(actionsByKey) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(actionsByKey)
|
||||||
|
.filter(([, action]) => action.effect)
|
||||||
|
.map(([key, action]) => [key, action.effect])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildActionLabels(actionsByKey, timers) {
|
||||||
|
const actionLabels = Object.fromEntries(
|
||||||
|
Object.entries(actionsByKey)
|
||||||
|
.filter(([, action]) => action.label)
|
||||||
|
.map(([key, action]) => [key, action.label])
|
||||||
|
)
|
||||||
|
|
||||||
|
Object.entries(timers || {}).forEach(([timerKey, timer]) => {
|
||||||
|
if (timer.historyLabel) {
|
||||||
|
actionLabels[`timer_used_${timerKey}`] = timer.historyLabel
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return actionLabels
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProgressionRules(actionsByKey) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(actionsByKey)
|
||||||
|
.filter(([, action]) => Array.isArray(action.progression))
|
||||||
|
.map(([key, action]) => [key, action.progression])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMatchStateConfig(rulesetId, bracketPosition) {
|
||||||
|
const ruleset = RULESETS[rulesetId] || RULESETS.folkstyle_usa
|
||||||
|
const phaseStyleKey = phaseStyleKeyForBracketPosition(bracketPosition)
|
||||||
|
const phaseStyle = ruleset.phases[phaseStyleKey]
|
||||||
|
|
||||||
|
return {
|
||||||
|
...ruleset,
|
||||||
|
actionEffects: buildActionEffects(ruleset.actionsByKey),
|
||||||
|
actionLabels: buildActionLabels(ruleset.actionsByKey, ruleset.timers),
|
||||||
|
progressionRules: buildProgressionRules(ruleset.actionsByKey),
|
||||||
|
matchFormat: { id: phaseStyleKey, label: phaseStyle.label },
|
||||||
|
phaseSequence: phaseStyle.sequence
|
||||||
|
}
|
||||||
|
}
|
||||||
567
app/assets/javascripts/lib/match_state/engine.js
Normal file
567
app/assets/javascripts/lib/match_state/engine.js
Normal file
@@ -0,0 +1,567 @@
|
|||||||
|
export function buildTimerState(config) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(config.timers).map(([timerKey, timerConfig]) => [
|
||||||
|
timerKey,
|
||||||
|
{
|
||||||
|
remainingSeconds: timerConfig.maxSeconds,
|
||||||
|
running: false,
|
||||||
|
startedAt: null
|
||||||
|
}
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildClockState(config) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
config.phaseSequence
|
||||||
|
.filter((phase) => phase.type === "period")
|
||||||
|
.map((phase) => [
|
||||||
|
phase.key,
|
||||||
|
{
|
||||||
|
durationSeconds: phase.clockSeconds,
|
||||||
|
remainingSeconds: phase.clockSeconds,
|
||||||
|
running: false,
|
||||||
|
startedAt: null
|
||||||
|
}
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildInitialState(config) {
|
||||||
|
const openingPhase = config.phaseSequence[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
participantScores: {
|
||||||
|
w1: 0,
|
||||||
|
w2: 0
|
||||||
|
},
|
||||||
|
control: "neutral",
|
||||||
|
displayControl: "neutral",
|
||||||
|
phaseIndex: 0,
|
||||||
|
selections: {},
|
||||||
|
assignment: {
|
||||||
|
w1: "green",
|
||||||
|
w2: "red"
|
||||||
|
},
|
||||||
|
nextEventId: 1,
|
||||||
|
nextEventGroupId: 1,
|
||||||
|
events: [],
|
||||||
|
clocksByPhase: buildClockState(config),
|
||||||
|
clock: {
|
||||||
|
durationSeconds: openingPhase.clockSeconds,
|
||||||
|
remainingSeconds: openingPhase.clockSeconds,
|
||||||
|
running: false,
|
||||||
|
startedAt: null
|
||||||
|
},
|
||||||
|
timers: {
|
||||||
|
w1: buildTimerState(config),
|
||||||
|
w2: buildTimerState(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildEvent(state, phase, clockSeconds, participantKey, actionKey, options = {}) {
|
||||||
|
return {
|
||||||
|
id: state.nextEventId++,
|
||||||
|
phaseKey: phase.key,
|
||||||
|
phaseLabel: phase.label,
|
||||||
|
clockSeconds,
|
||||||
|
participantKey,
|
||||||
|
actionKey,
|
||||||
|
actionGroupId: options.actionGroupId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function opponentParticipant(participantKey) {
|
||||||
|
return participantKey === "w1" ? "w2" : "w1"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isProgressiveAction(config, actionKey) {
|
||||||
|
return Object.prototype.hasOwnProperty.call(config.progressionRules || {}, actionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function progressiveActionCountForParticipant(events, participantKey, actionKey) {
|
||||||
|
return events.filter((eventRecord) =>
|
||||||
|
eventRecord.participantKey === participantKey && eventRecord.actionKey === actionKey
|
||||||
|
).length
|
||||||
|
}
|
||||||
|
|
||||||
|
export function progressiveActionPointsForOffense(config, actionKey, offenseNumber) {
|
||||||
|
const progression = config.progressionRules?.[actionKey] || []
|
||||||
|
return progression[Math.min(offenseNumber - 1, progression.length - 1)] || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recordProgressiveAction(config, state, participantKey, actionKey, buildEvent) {
|
||||||
|
const offenseNumber = progressiveActionCountForParticipant(state.events, participantKey, actionKey) + 1
|
||||||
|
const actionGroupId = state.nextEventGroupId++
|
||||||
|
state.events.push(buildEvent(participantKey, actionKey, { actionGroupId }))
|
||||||
|
|
||||||
|
const awardedPoints = progressiveActionPointsForOffense(config, actionKey, offenseNumber)
|
||||||
|
if (awardedPoints > 0) {
|
||||||
|
state.events.push(buildEvent(opponentParticipant(participantKey), `plus_${awardedPoints}`, { actionGroupId }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyMatchAction(config, state, phase, clockSeconds, participantKey, actionKey) {
|
||||||
|
const effect = config.actionEffects[actionKey]
|
||||||
|
if (!effect) return false
|
||||||
|
|
||||||
|
if (isProgressiveAction(config, actionKey)) {
|
||||||
|
recordProgressiveAction(
|
||||||
|
config,
|
||||||
|
state,
|
||||||
|
participantKey,
|
||||||
|
actionKey,
|
||||||
|
(eventParticipantKey, eventActionKey, options = {}) =>
|
||||||
|
buildEvent(state, phase, clockSeconds, eventParticipantKey, eventActionKey, options)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
state.events.push(buildEvent(state, phase, clockSeconds, participantKey, actionKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyChoiceAction(state, phase, clockSeconds, participantKey, choiceKey) {
|
||||||
|
if (phase.type !== "choice") return { applied: false, deferred: false }
|
||||||
|
|
||||||
|
state.events.push(buildEvent(state, phase, clockSeconds, participantKey, `choice_${choiceKey}`))
|
||||||
|
|
||||||
|
if (choiceKey === "defer") {
|
||||||
|
return { applied: true, deferred: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
state.selections[phase.key] = {
|
||||||
|
participantKey,
|
||||||
|
choiceKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return { applied: true, deferred: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteEventFromState(config, state, eventId) {
|
||||||
|
const eventRecord = state.events.find((eventItem) => eventItem.id === eventId)
|
||||||
|
if (!eventRecord) return false
|
||||||
|
|
||||||
|
let eventIdsToDelete = [eventId]
|
||||||
|
|
||||||
|
if (eventRecord.actionKey.startsWith("timer_used_") && typeof eventRecord.elapsedSeconds === "number") {
|
||||||
|
const timerKey = eventRecord.actionKey.replace("timer_used_", "")
|
||||||
|
const timer = state.timers[eventRecord.participantKey]?.[timerKey]
|
||||||
|
const maxSeconds = config.timers[timerKey]?.maxSeconds || 0
|
||||||
|
if (timer) {
|
||||||
|
timer.remainingSeconds = Math.min(maxSeconds, timer.remainingSeconds + eventRecord.elapsedSeconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isProgressiveAction(config, eventRecord.actionKey)) {
|
||||||
|
const linkedAward = findLinkedProgressiveAward(state.events, eventRecord)
|
||||||
|
if (linkedAward) {
|
||||||
|
eventIdsToDelete.push(linkedAward.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.events = state.events.filter((eventItem) => !eventIdsToDelete.includes(eventItem.id))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function swapEventParticipants(config, state, eventId) {
|
||||||
|
const eventRecord = state.events.find((eventItem) => eventItem.id === eventId)
|
||||||
|
if (!eventRecord) return false
|
||||||
|
|
||||||
|
const originalParticipant = eventRecord.participantKey
|
||||||
|
const swappedParticipant = opponentParticipant(originalParticipant)
|
||||||
|
|
||||||
|
if (eventRecord.actionKey.startsWith("timer_used_") && typeof eventRecord.elapsedSeconds === "number") {
|
||||||
|
reassignTimerUsage(config, state, eventRecord, swappedParticipant)
|
||||||
|
}
|
||||||
|
|
||||||
|
eventRecord.participantKey = swappedParticipant
|
||||||
|
|
||||||
|
if (isProgressiveAction(config, eventRecord.actionKey)) {
|
||||||
|
swapLinkedProgressiveAward(state.events, eventRecord, swappedParticipant)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function swapPhaseParticipants(config, state, phaseKey) {
|
||||||
|
const phaseEvents = state.events.filter((eventRecord) => eventRecord.phaseKey === phaseKey)
|
||||||
|
if (phaseEvents.length === 0) return false
|
||||||
|
|
||||||
|
phaseEvents.forEach((eventRecord) => {
|
||||||
|
if (eventRecord.actionKey.startsWith("timer_used_") && typeof eventRecord.elapsedSeconds === "number") {
|
||||||
|
reassignTimerUsage(config, state, eventRecord, opponentParticipant(eventRecord.participantKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
eventRecord.participantKey = opponentParticipant(eventRecord.participantKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function phaseIndexForKey(config, phaseKey) {
|
||||||
|
const phaseIndex = config.phaseSequence.findIndex((phase) => phase.key === phaseKey)
|
||||||
|
return phaseIndex === -1 ? Number.MAX_SAFE_INTEGER : phaseIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activeClockForPhase(state, phase) {
|
||||||
|
if (!phase || phase.type !== "period") return null
|
||||||
|
return state.clocksByPhase[phase.key] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasRunningClockOrTimer(state) {
|
||||||
|
const anyTimerRunning = ["w1", "w2"].some((participantKey) =>
|
||||||
|
Object.values(state.timers[participantKey] || {}).some((timer) => timer.running)
|
||||||
|
)
|
||||||
|
const anyClockRunning = Object.values(state.clocksByPhase || {}).some((clock) => clock.running)
|
||||||
|
return anyTimerRunning || anyClockRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopAllAuxiliaryTimers(state, now = Date.now()) {
|
||||||
|
for (const participantKey of ["w1", "w2"]) {
|
||||||
|
for (const timerKey of Object.keys(state.timers[participantKey] || {})) {
|
||||||
|
const timer = state.timers[participantKey][timerKey]
|
||||||
|
if (!timer.running) continue
|
||||||
|
|
||||||
|
const elapsedSeconds = Math.floor((now - timer.startedAt) / 1000)
|
||||||
|
timer.remainingSeconds = Math.max(0, timer.remainingSeconds - elapsedSeconds)
|
||||||
|
timer.running = false
|
||||||
|
timer.startedAt = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function moveToPreviousPhase(config, state) {
|
||||||
|
if (state.phaseIndex === 0) return false
|
||||||
|
state.phaseIndex -= 1
|
||||||
|
state.control = baseControlForPhase(config.phaseSequence[state.phaseIndex], state.selections, state.control)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function moveToNextPhase(config, state) {
|
||||||
|
if (state.phaseIndex >= config.phaseSequence.length - 1) return false
|
||||||
|
state.phaseIndex += 1
|
||||||
|
state.control = baseControlForPhase(config.phaseSequence[state.phaseIndex], state.selections, state.control)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function orderedEvents(config, events) {
|
||||||
|
return [...events].sort((leftEvent, rightEvent) => {
|
||||||
|
const leftPhaseIndex = phaseIndexForKey(config, leftEvent.phaseKey)
|
||||||
|
const rightPhaseIndex = phaseIndexForKey(config, rightEvent.phaseKey)
|
||||||
|
if (leftPhaseIndex !== rightPhaseIndex) {
|
||||||
|
return leftPhaseIndex - rightPhaseIndex
|
||||||
|
}
|
||||||
|
return leftEvent.id - rightEvent.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function controlFromChoice(selection) {
|
||||||
|
if (!selection) return "neutral"
|
||||||
|
if (selection.choiceKey === "neutral" || selection.choiceKey === "defer") return "neutral"
|
||||||
|
if (selection.choiceKey === "top") return `${selection.participantKey}_control`
|
||||||
|
if (selection.choiceKey === "bottom") return `${opponentParticipant(selection.participantKey)}_control`
|
||||||
|
return "neutral"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function baseControlForPhase(phase, selections, fallbackControl) {
|
||||||
|
if (phase.type !== "period") return fallbackControl
|
||||||
|
if (phase.startsIn === "neutral") return "neutral"
|
||||||
|
if (phase.startsFromChoice) {
|
||||||
|
return controlFromChoice(selections[phase.startsFromChoice])
|
||||||
|
}
|
||||||
|
return "neutral"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recomputeDerivedState(config, state) {
|
||||||
|
state.participantScores = { w1: 0, w2: 0 }
|
||||||
|
state.selections = {}
|
||||||
|
state.control = baseControlForPhase(config.phaseSequence[state.phaseIndex], state.selections, state.control)
|
||||||
|
|
||||||
|
orderedEvents(config, state.events).forEach((eventRecord) => {
|
||||||
|
if (eventRecord.actionKey.startsWith("choice_")) {
|
||||||
|
const choiceKey = eventRecord.actionKey.replace("choice_", "")
|
||||||
|
if (choiceKey === "defer") return
|
||||||
|
|
||||||
|
state.selections[eventRecord.phaseKey] = {
|
||||||
|
participantKey: eventRecord.participantKey,
|
||||||
|
choiceKey: choiceKey
|
||||||
|
}
|
||||||
|
state.control = baseControlForPhase(config.phaseSequence[state.phaseIndex], state.selections, state.control)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const effect = config.actionEffects[eventRecord.actionKey]
|
||||||
|
if (!effect) return
|
||||||
|
|
||||||
|
const scoringParticipant = effect.target === "opponent"
|
||||||
|
? opponentParticipant(eventRecord.participantKey)
|
||||||
|
: eventRecord.participantKey
|
||||||
|
const nextScore = state.participantScores[scoringParticipant] + effect.points
|
||||||
|
state.participantScores[scoringParticipant] = Math.max(0, nextScore)
|
||||||
|
|
||||||
|
if (effect.nextPosition === "neutral") {
|
||||||
|
state.control = "neutral"
|
||||||
|
} else if (effect.nextPosition === "top") {
|
||||||
|
state.control = `${eventRecord.participantKey}_control`
|
||||||
|
} else if (effect.nextPosition === "bottom") {
|
||||||
|
state.control = `${opponentParticipant(eventRecord.participantKey)}_control`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
state.displayControl = controlForSelectedPhase(config, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function controlForSelectedPhase(config, state) {
|
||||||
|
const selectedPhase = config.phaseSequence[state.phaseIndex]
|
||||||
|
let control = baseControlForPhase(selectedPhase, state.selections, state.control)
|
||||||
|
const selectedPhaseIndex = phaseIndexForKey(config, selectedPhase.key)
|
||||||
|
|
||||||
|
orderedEvents(config, state.events).forEach((eventRecord) => {
|
||||||
|
if (phaseIndexForKey(config, eventRecord.phaseKey) > selectedPhaseIndex) return
|
||||||
|
if (eventRecord.phaseKey !== selectedPhase.key) return
|
||||||
|
|
||||||
|
const effect = config.actionEffects[eventRecord.actionKey]
|
||||||
|
if (!effect) return
|
||||||
|
|
||||||
|
if (effect.nextPosition === "neutral") {
|
||||||
|
control = "neutral"
|
||||||
|
} else if (effect.nextPosition === "top") {
|
||||||
|
control = `${eventRecord.participantKey}_control`
|
||||||
|
} else if (effect.nextPosition === "bottom") {
|
||||||
|
control = `${opponentParticipant(eventRecord.participantKey)}_control`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return control
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentClockSeconds(clockState, now = Date.now()) {
|
||||||
|
if (!clockState) return 0
|
||||||
|
if (!clockState.running || !clockState.startedAt) {
|
||||||
|
return clockState.remainingSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsedSeconds = Math.floor((now - clockState.startedAt) / 1000)
|
||||||
|
return Math.max(0, clockState.remainingSeconds - elapsedSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentAuxiliaryTimerSeconds(timerState, now = Date.now()) {
|
||||||
|
if (!timerState) return 0
|
||||||
|
if (!timerState.running || !timerState.startedAt) {
|
||||||
|
return timerState.remainingSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsedSeconds = Math.floor((now - timerState.startedAt) / 1000)
|
||||||
|
return Math.max(0, timerState.remainingSeconds - elapsedSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncClockSnapshot(activeClock) {
|
||||||
|
if (!activeClock) {
|
||||||
|
return {
|
||||||
|
durationSeconds: 0,
|
||||||
|
remainingSeconds: 0,
|
||||||
|
running: false,
|
||||||
|
startedAt: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
durationSeconds: activeClock.durationSeconds,
|
||||||
|
remainingSeconds: activeClock.remainingSeconds,
|
||||||
|
running: activeClock.running,
|
||||||
|
startedAt: activeClock.startedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startClockState(activeClock, now = Date.now()) {
|
||||||
|
if (!activeClock || activeClock.running) return false
|
||||||
|
activeClock.running = true
|
||||||
|
activeClock.startedAt = now
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopClockState(activeClock, now = Date.now()) {
|
||||||
|
if (!activeClock || !activeClock.running) return false
|
||||||
|
activeClock.remainingSeconds = currentClockSeconds(activeClock, now)
|
||||||
|
activeClock.running = false
|
||||||
|
activeClock.startedAt = null
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function adjustClockState(activeClock, deltaSeconds, now = Date.now()) {
|
||||||
|
if (!activeClock) return false
|
||||||
|
|
||||||
|
const currentSeconds = currentClockSeconds(activeClock, now)
|
||||||
|
activeClock.remainingSeconds = Math.max(0, currentSeconds + deltaSeconds)
|
||||||
|
if (activeClock.running) {
|
||||||
|
activeClock.startedAt = now
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startAuxiliaryTimerState(timerState, now = Date.now()) {
|
||||||
|
if (!timerState || timerState.running) return false
|
||||||
|
timerState.running = true
|
||||||
|
timerState.startedAt = now
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopAuxiliaryTimerState(timerState, now = Date.now()) {
|
||||||
|
if (!timerState || !timerState.running) return { stopped: false, elapsedSeconds: 0 }
|
||||||
|
|
||||||
|
const elapsedSeconds = Math.floor((now - timerState.startedAt) / 1000)
|
||||||
|
timerState.remainingSeconds = currentAuxiliaryTimerSeconds(timerState, now)
|
||||||
|
timerState.running = false
|
||||||
|
timerState.startedAt = null
|
||||||
|
|
||||||
|
return { stopped: true, elapsedSeconds }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function accumulatedMatchSeconds(config, state, activePhaseKey, now = Date.now()) {
|
||||||
|
return config.phaseSequence
|
||||||
|
.filter((phase) => phase.type === "period")
|
||||||
|
.reduce((totalElapsed, phase) => {
|
||||||
|
const clockState = state.clocksByPhase[phase.key]
|
||||||
|
if (!clockState) return totalElapsed
|
||||||
|
|
||||||
|
const remainingSeconds = phase.key === activePhaseKey
|
||||||
|
? currentClockSeconds(clockState, now)
|
||||||
|
: clockState.remainingSeconds
|
||||||
|
|
||||||
|
const elapsedSeconds = Math.max(0, clockState.durationSeconds - remainingSeconds)
|
||||||
|
return totalElapsed + elapsedSeconds
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function derivedStats(config, events) {
|
||||||
|
const grouped = config.phaseSequence.map((phase) => {
|
||||||
|
const phaseEvents = orderedEvents(config, events).filter((eventRecord) => eventRecord.phaseKey === phase.key)
|
||||||
|
if (phaseEvents.length === 0) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: phase.label,
|
||||||
|
w1: phaseEvents
|
||||||
|
.filter((eventRecord) => eventRecord.participantKey === "w1")
|
||||||
|
.map((eventRecord) => statTextForEvent(config, eventRecord))
|
||||||
|
.filter(Boolean),
|
||||||
|
w2: phaseEvents
|
||||||
|
.filter((eventRecord) => eventRecord.participantKey === "w2")
|
||||||
|
.map((eventRecord) => statTextForEvent(config, eventRecord))
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
}).filter(Boolean)
|
||||||
|
|
||||||
|
return {
|
||||||
|
w1: formatStatsByPhase(grouped, "w1"),
|
||||||
|
w2: formatStatsByPhase(grouped, "w2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scoreboardStatePayload(config, state, metadata) {
|
||||||
|
return {
|
||||||
|
participantScores: state.participantScores,
|
||||||
|
assignment: state.assignment,
|
||||||
|
phaseIndex: state.phaseIndex,
|
||||||
|
clocksByPhase: state.clocksByPhase,
|
||||||
|
timers: state.timers,
|
||||||
|
metadata: metadata,
|
||||||
|
matchResult: {
|
||||||
|
finished: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchResultDefaults(state, options = {}) {
|
||||||
|
const {
|
||||||
|
w1Id = "",
|
||||||
|
w2Id = "",
|
||||||
|
currentPhase = {},
|
||||||
|
accumulationSeconds = 0
|
||||||
|
} = options
|
||||||
|
|
||||||
|
const w1Score = state.participantScores.w1
|
||||||
|
const w2Score = state.participantScores.w2
|
||||||
|
let winnerId = ""
|
||||||
|
let winnerScore = w1Score
|
||||||
|
let loserScore = w2Score
|
||||||
|
|
||||||
|
if (w1Score > w2Score) {
|
||||||
|
winnerId = w1Id || ""
|
||||||
|
winnerScore = w1Score
|
||||||
|
loserScore = w2Score
|
||||||
|
} else if (w2Score > w1Score) {
|
||||||
|
winnerId = w2Id || ""
|
||||||
|
winnerScore = w2Score
|
||||||
|
loserScore = w1Score
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
winnerId,
|
||||||
|
overtimeType: currentPhase.overtimeType || "",
|
||||||
|
winnerScore,
|
||||||
|
loserScore,
|
||||||
|
pinMinutes: Math.floor(accumulationSeconds / 60),
|
||||||
|
pinSeconds: accumulationSeconds % 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function statTextForEvent(config, eventRecord) {
|
||||||
|
if (eventRecord.actionKey.startsWith("timer_used_")) {
|
||||||
|
const timerKey = eventRecord.actionKey.replace("timer_used_", "")
|
||||||
|
const timerConfig = config.timers[timerKey]
|
||||||
|
if (!timerConfig || typeof eventRecord.elapsedSeconds !== "number") return null
|
||||||
|
return `${timerConfig.statCode || timerConfig.label}: ${formatClock(eventRecord.elapsedSeconds)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = config.actionsByKey[eventRecord.actionKey]
|
||||||
|
return action?.statCode || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatStatsByPhase(groupedPhases, participantKey) {
|
||||||
|
return groupedPhases
|
||||||
|
.map((phase) => {
|
||||||
|
const items = phase[participantKey]
|
||||||
|
if (!items || items.length === 0) return null
|
||||||
|
return `${phase.label}: ${items.join(" ")}`
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatClock(totalSeconds) {
|
||||||
|
const minutes = Math.floor(totalSeconds / 60)
|
||||||
|
const seconds = totalSeconds % 60
|
||||||
|
return `${minutes}:${seconds.toString().padStart(2, "0")}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function reassignTimerUsage(config, state, eventRecord, newParticipantKey) {
|
||||||
|
const timerKey = eventRecord.actionKey.replace("timer_used_", "")
|
||||||
|
const originalParticipant = eventRecord.participantKey
|
||||||
|
const originalTimer = state.timers[originalParticipant]?.[timerKey]
|
||||||
|
const newTimer = state.timers[newParticipantKey]?.[timerKey]
|
||||||
|
const maxSeconds = config.timers[timerKey]?.maxSeconds || 0
|
||||||
|
|
||||||
|
if (!originalTimer || !newTimer || typeof eventRecord.elapsedSeconds !== "number") return
|
||||||
|
|
||||||
|
originalTimer.remainingSeconds = Math.min(maxSeconds, originalTimer.remainingSeconds + eventRecord.elapsedSeconds)
|
||||||
|
newTimer.remainingSeconds = Math.max(0, newTimer.remainingSeconds - eventRecord.elapsedSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
function swapLinkedProgressiveAward(events, eventRecord, offendingParticipant) {
|
||||||
|
const linkedAward = findLinkedProgressiveAward(events, eventRecord)
|
||||||
|
if (linkedAward) {
|
||||||
|
linkedAward.participantKey = opponentParticipant(offendingParticipant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findLinkedProgressiveAward(events, eventRecord) {
|
||||||
|
return events.find((candidateEvent) =>
|
||||||
|
candidateEvent.id !== eventRecord.id &&
|
||||||
|
candidateEvent.actionGroupId &&
|
||||||
|
candidateEvent.actionGroupId === eventRecord.actionGroupId &&
|
||||||
|
candidateEvent.actionKey.startsWith("plus_")
|
||||||
|
)
|
||||||
|
}
|
||||||
94
app/assets/javascripts/lib/match_state/presenters.js
Normal file
94
app/assets/javascripts/lib/match_state/presenters.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { orderedEvents } from "match-state-engine"
|
||||||
|
|
||||||
|
export function displayLabelForParticipant(assignment, participantKey) {
|
||||||
|
return assignment[participantKey] === "green" ? "Green" : "Red"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buttonClassForParticipant(assignment, participantKey) {
|
||||||
|
return assignment[participantKey] === "green" ? "btn-success" : "btn-danger"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function humanizeChoice(choiceKey) {
|
||||||
|
if (choiceKey === "top") return "Top"
|
||||||
|
if (choiceKey === "bottom") return "Bottom"
|
||||||
|
if (choiceKey === "neutral") return "Neutral"
|
||||||
|
if (choiceKey === "defer") return "Defer"
|
||||||
|
return choiceKey
|
||||||
|
}
|
||||||
|
|
||||||
|
export function choiceLabelForPhase(phase) {
|
||||||
|
if (phase.chooser === "other") return "Other wrestler chooses"
|
||||||
|
return "Choose wrestler and position"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function eventLogSections(config, state, formatClock) {
|
||||||
|
const eventsByPhase = orderedEvents(config, state.events).reduce((accumulator, eventRecord) => {
|
||||||
|
if (!accumulator[eventRecord.phaseKey]) {
|
||||||
|
accumulator[eventRecord.phaseKey] = []
|
||||||
|
}
|
||||||
|
accumulator[eventRecord.phaseKey].push(eventRecord)
|
||||||
|
return accumulator
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
return config.phaseSequence.map((phase) => {
|
||||||
|
const phaseEvents = eventsByPhase[phase.key]
|
||||||
|
if (!phaseEvents || phaseEvents.length === 0) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: phase.key,
|
||||||
|
label: phase.label,
|
||||||
|
items: [...phaseEvents].reverse().map((eventRecord) => ({
|
||||||
|
id: eventRecord.id,
|
||||||
|
participantKey: eventRecord.participantKey,
|
||||||
|
colorLabel: displayLabelForParticipant(state.assignment, eventRecord.participantKey),
|
||||||
|
actionLabel: eventActionLabel(config, eventRecord, formatClock),
|
||||||
|
clockLabel: formatClock(eventRecord.clockSeconds)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function choiceViewModel(config, state, phase, participantMeta) {
|
||||||
|
if (phase.type !== "choice") return null
|
||||||
|
|
||||||
|
const phaseEvents = state.events.filter((eventRecord) => eventRecord.phaseKey === phase.key)
|
||||||
|
const deferredParticipants = phaseEvents
|
||||||
|
.filter((eventRecord) => eventRecord.actionKey === "choice_defer")
|
||||||
|
.map((eventRecord) => eventRecord.participantKey)
|
||||||
|
const selection = state.selections[phase.key]
|
||||||
|
|
||||||
|
const selectionText = selection
|
||||||
|
? `Selected: ${displayLabelForParticipant(state.assignment, selection.participantKey)} ${humanizeChoice(selection.choiceKey)}`
|
||||||
|
: deferredParticipants.length > 0
|
||||||
|
? `${deferredParticipants.map((participantKey) => displayLabelForParticipant(state.assignment, participantKey)).join(", ")} deferred. Waiting for the other wrestler to choose.`
|
||||||
|
: "No choice selected."
|
||||||
|
|
||||||
|
const availableParticipants = deferredParticipants.length > 0
|
||||||
|
? ["w1", "w2"].filter((participantKey) => !deferredParticipants.includes(participantKey))
|
||||||
|
: ["w1", "w2"]
|
||||||
|
|
||||||
|
const buttons = availableParticipants.flatMap((participantKey) =>
|
||||||
|
phase.options
|
||||||
|
.filter((choiceKey) => !(deferredParticipants.length > 0 && choiceKey === "defer"))
|
||||||
|
.map((choiceKey) => ({
|
||||||
|
participantKey,
|
||||||
|
choiceKey,
|
||||||
|
buttonClass: buttonClassForParticipant(state.assignment, participantKey),
|
||||||
|
text: `${participantMeta[participantKey].name} (${displayLabelForParticipant(state.assignment, participantKey)}) ${humanizeChoice(choiceKey)}`
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: choiceLabelForPhase(phase),
|
||||||
|
selectionText,
|
||||||
|
buttons
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventActionLabel(config, eventRecord, formatClock) {
|
||||||
|
let actionLabel = config.actionLabels[eventRecord.actionKey] || eventRecord.actionKey
|
||||||
|
if (eventRecord.actionKey.startsWith("timer_used_") && typeof eventRecord.elapsedSeconds === "number") {
|
||||||
|
actionLabel = `${actionLabel}: ${formatClock(eventRecord.elapsedSeconds)}`
|
||||||
|
}
|
||||||
|
return actionLabel
|
||||||
|
}
|
||||||
288
app/assets/javascripts/lib/match_state/scoreboard_presenters.js
Normal file
288
app/assets/javascripts/lib/match_state/scoreboard_presenters.js
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
export function participantForColor(state, color) {
|
||||||
|
if (!state?.assignment) {
|
||||||
|
return color === "red" ? "w2" : "w1"
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = Object.entries(state.assignment).find(([, assignedColor]) => assignedColor === color)
|
||||||
|
return match ? match[0] : (color === "red" ? "w2" : "w1")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function participantColor(state, participantKey) {
|
||||||
|
return state?.assignment?.[participantKey] || (participantKey === "w1" ? "green" : "red")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function participantName(state, participantKey) {
|
||||||
|
return participantKey === "w1" ? state?.metadata?.w1Name : state?.metadata?.w2Name
|
||||||
|
}
|
||||||
|
|
||||||
|
export function participantSchool(state, participantKey) {
|
||||||
|
return participantKey === "w1" ? state?.metadata?.w1School : state?.metadata?.w2School
|
||||||
|
}
|
||||||
|
|
||||||
|
export function participantScore(state, participantKey) {
|
||||||
|
return state?.participantScores?.[participantKey] || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentPhaseLabel(config, state) {
|
||||||
|
const phaseIndex = state?.phaseIndex || 0
|
||||||
|
return config?.phaseSequence?.[phaseIndex]?.label || "Period 1"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentClockText(config, state, formatClock, now = Date.now()) {
|
||||||
|
const phaseIndex = state?.phaseIndex || 0
|
||||||
|
const phase = config?.phaseSequence?.[phaseIndex]
|
||||||
|
if (!phase || phase.type !== "period") return "-"
|
||||||
|
|
||||||
|
const clockState = state?.clocksByPhase?.[phase.key]
|
||||||
|
if (!clockState) return formatClock(phase.clockSeconds)
|
||||||
|
|
||||||
|
let remainingSeconds = clockState.remainingSeconds
|
||||||
|
if (clockState.running && clockState.startedAt) {
|
||||||
|
const elapsedSeconds = Math.floor((now - clockState.startedAt) / 1000)
|
||||||
|
remainingSeconds = Math.max(0, clockState.remainingSeconds - elapsedSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatClock(remainingSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentAuxiliaryTimerSeconds(state, participantKey, timerKey, now = Date.now()) {
|
||||||
|
const timer = state?.timers?.[participantKey]?.[timerKey]
|
||||||
|
if (!timer) return 0
|
||||||
|
if (!timer.running || !timer.startedAt) {
|
||||||
|
return timer.remainingSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsedSeconds = Math.floor((now - timer.startedAt) / 1000)
|
||||||
|
return Math.max(0, timer.remainingSeconds - elapsedSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runningTimerForParticipant(state, participantKey) {
|
||||||
|
for (const timerKey of Object.keys(state?.timers?.[participantKey] || {})) {
|
||||||
|
if (state.timers[participantKey][timerKey]?.running) {
|
||||||
|
return timerKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function participantDisplayLabel(state, participantKey) {
|
||||||
|
return `${participantForColor(state, "red") === participantKey ? "Red" : "Green"} ${participantName(state, participantKey)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function timerIndicatorLabel(config, state, participantKey, formatClock, now = Date.now()) {
|
||||||
|
const runningTimer = runningTimerForParticipant(state, participantKey)
|
||||||
|
if (!runningTimer) return ""
|
||||||
|
|
||||||
|
const timerConfig = config?.timers?.[runningTimer]
|
||||||
|
if (!timerConfig) return ""
|
||||||
|
|
||||||
|
const remainingSeconds = currentAuxiliaryTimerSeconds(state, participantKey, runningTimer, now)
|
||||||
|
const usedSeconds = Math.max(0, timerConfig.maxSeconds - remainingSeconds)
|
||||||
|
return `${timerConfig.label}: ${formatClock(usedSeconds)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildRunningTimerSnapshot(state) {
|
||||||
|
const snapshot = {}
|
||||||
|
for (const participantKey of ["w1", "w2"]) {
|
||||||
|
for (const timerKey of Object.keys(state?.timers?.[participantKey] || {})) {
|
||||||
|
const timer = state.timers[participantKey][timerKey]
|
||||||
|
snapshot[`${participantKey}:${timerKey}`] = Boolean(timer?.running)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return snapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
export function detectRecentlyStoppedTimer(state, previousTimerSnapshot) {
|
||||||
|
previousTimerSnapshot ||= {}
|
||||||
|
|
||||||
|
for (const participantKey of ["w1", "w2"]) {
|
||||||
|
for (const timerKey of Object.keys(state?.timers?.[participantKey] || {})) {
|
||||||
|
const snapshotKey = `${participantKey}:${timerKey}`
|
||||||
|
const wasRunning = previousTimerSnapshot[snapshotKey]
|
||||||
|
const isRunning = Boolean(state.timers[participantKey][timerKey]?.running)
|
||||||
|
if (wasRunning && !isRunning) {
|
||||||
|
return { participantKey, timerKey }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runningAuxiliaryTimer(state) {
|
||||||
|
for (const participantKey of ["w1", "w2"]) {
|
||||||
|
for (const timerKey of Object.keys(state?.timers?.[participantKey] || {})) {
|
||||||
|
const timer = state.timers[participantKey][timerKey]
|
||||||
|
if (timer?.running) {
|
||||||
|
return { participantKey, timerKey }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mainClockRunning(config, state) {
|
||||||
|
const phaseIndex = state?.phaseIndex || 0
|
||||||
|
const phase = config?.phaseSequence?.[phaseIndex]
|
||||||
|
if (!phase || phase.type !== "period") return false
|
||||||
|
return Boolean(state?.clocksByPhase?.[phase.key]?.running)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function timerBannerViewModel(config, state, timerBannerState, formatClock, now = Date.now()) {
|
||||||
|
if (!timerBannerState) return null
|
||||||
|
|
||||||
|
const { participantKey, timerKey, expiresAt } = timerBannerState
|
||||||
|
if (expiresAt && now > expiresAt) return null
|
||||||
|
|
||||||
|
const timer = state?.timers?.[participantKey]?.[timerKey]
|
||||||
|
const timerConfig = config?.timers?.[timerKey]
|
||||||
|
if (!timer || !timerConfig) return null
|
||||||
|
|
||||||
|
const runningSeconds = currentAuxiliaryTimerSeconds(state, participantKey, timerKey, now)
|
||||||
|
const usedSeconds = Math.max(0, timerConfig.maxSeconds - runningSeconds)
|
||||||
|
const color = participantColor(state, participantKey)
|
||||||
|
const label = `${participantDisplayLabel(state, participantKey)} ${timerConfig.label}`
|
||||||
|
|
||||||
|
return {
|
||||||
|
color,
|
||||||
|
label: timer.running ? `${label} Running` : `${label} Used`,
|
||||||
|
clockText: formatClock(usedSeconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function populatedBoardViewModel(config, state, liveMatchData, currentBoutNumber, formatClock, now = Date.now()) {
|
||||||
|
const redParticipant = participantForColor(state, "red")
|
||||||
|
const greenParticipant = participantForColor(state, "green")
|
||||||
|
|
||||||
|
return {
|
||||||
|
isEmpty: false,
|
||||||
|
redName: participantName(state, redParticipant),
|
||||||
|
redSchool: participantSchool(state, redParticipant),
|
||||||
|
redScore: participantScore(state, redParticipant).toString(),
|
||||||
|
redTimerIndicator: timerIndicatorLabel(config, state, redParticipant, formatClock, now),
|
||||||
|
greenName: participantName(state, greenParticipant),
|
||||||
|
greenSchool: participantSchool(state, greenParticipant),
|
||||||
|
greenScore: participantScore(state, greenParticipant).toString(),
|
||||||
|
greenTimerIndicator: timerIndicatorLabel(config, state, greenParticipant, formatClock, now),
|
||||||
|
clockText: currentClockText(config, state, formatClock, now),
|
||||||
|
phaseLabel: currentPhaseLabel(config, state),
|
||||||
|
weightLabel: state?.metadata?.weightLabel ? `Weight ${state.metadata.weightLabel}` : "Weight -",
|
||||||
|
boutLabel: currentBoutNumber ? `Bout ${currentBoutNumber}` : "No Bout",
|
||||||
|
redStats: redParticipant === "w1" ? (liveMatchData?.w1_stat || "") : (liveMatchData?.w2_stat || ""),
|
||||||
|
greenStats: greenParticipant === "w1" ? (liveMatchData?.w1_stat || "") : (liveMatchData?.w2_stat || "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emptyBoardViewModel(currentBoutNumber, lastMatchResult) {
|
||||||
|
return {
|
||||||
|
isEmpty: true,
|
||||||
|
redName: "NO MATCH",
|
||||||
|
redSchool: "",
|
||||||
|
redScore: "0",
|
||||||
|
redTimerIndicator: "",
|
||||||
|
greenName: "NO MATCH",
|
||||||
|
greenSchool: "",
|
||||||
|
greenScore: "0",
|
||||||
|
greenTimerIndicator: "",
|
||||||
|
clockText: "-",
|
||||||
|
phaseLabel: "No Match",
|
||||||
|
weightLabel: "Weight -",
|
||||||
|
boutLabel: currentBoutNumber ? `Bout ${currentBoutNumber}` : "No Bout",
|
||||||
|
redStats: "",
|
||||||
|
greenStats: "",
|
||||||
|
lastMatchResult: lastMatchResult || "-"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nextTimerBannerState(state, previousTimerSnapshot, now = Date.now()) {
|
||||||
|
if (!state?.timers) {
|
||||||
|
return { timerBannerState: null, previousTimerSnapshot: {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeTimer = runningAuxiliaryTimer(state)
|
||||||
|
const nextSnapshot = buildRunningTimerSnapshot(state)
|
||||||
|
|
||||||
|
if (activeTimer) {
|
||||||
|
return {
|
||||||
|
timerBannerState: {
|
||||||
|
participantKey: activeTimer.participantKey,
|
||||||
|
timerKey: activeTimer.timerKey,
|
||||||
|
expiresAt: null
|
||||||
|
},
|
||||||
|
previousTimerSnapshot: nextSnapshot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stoppedTimer = detectRecentlyStoppedTimer(state, previousTimerSnapshot)
|
||||||
|
if (stoppedTimer) {
|
||||||
|
return {
|
||||||
|
timerBannerState: {
|
||||||
|
participantKey: stoppedTimer.participantKey,
|
||||||
|
timerKey: stoppedTimer.timerKey,
|
||||||
|
expiresAt: now + 10000
|
||||||
|
},
|
||||||
|
previousTimerSnapshot: nextSnapshot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
timerBannerState: null,
|
||||||
|
previousTimerSnapshot: nextSnapshot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function boardColors(isEmpty) {
|
||||||
|
if (isEmpty) {
|
||||||
|
return {
|
||||||
|
red: "#000",
|
||||||
|
center: "#000",
|
||||||
|
green: "#000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
red: "#c91f1f",
|
||||||
|
center: "#050505",
|
||||||
|
green: "#1cab2d"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function timerBannerRenderState(config, state, timerBannerState, formatClock, now = Date.now()) {
|
||||||
|
if (mainClockRunning(config, state)) {
|
||||||
|
return {
|
||||||
|
timerBannerState: timerBannerState?.expiresAt ? null : timerBannerState,
|
||||||
|
visible: false,
|
||||||
|
viewModel: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!timerBannerState) {
|
||||||
|
return {
|
||||||
|
timerBannerState: null,
|
||||||
|
visible: false,
|
||||||
|
viewModel: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timerBannerState.expiresAt && now > timerBannerState.expiresAt) {
|
||||||
|
return {
|
||||||
|
timerBannerState: null,
|
||||||
|
visible: false,
|
||||||
|
viewModel: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewModel = timerBannerViewModel(config, state, timerBannerState, formatClock, now)
|
||||||
|
if (!viewModel) {
|
||||||
|
return {
|
||||||
|
timerBannerState,
|
||||||
|
visible: false,
|
||||||
|
viewModel: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
timerBannerState,
|
||||||
|
visible: true,
|
||||||
|
viewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
158
app/assets/javascripts/lib/match_state/scoreboard_state.js
Normal file
158
app/assets/javascripts/lib/match_state/scoreboard_state.js
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import { buildStorageKey } from "match-state-serializers"
|
||||||
|
|
||||||
|
export function buildScoreboardContext({ initialBoutNumber, matchId }) {
|
||||||
|
const currentQueueBoutNumber = initialBoutNumber > 0 ? initialBoutNumber : null
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentQueueBoutNumber,
|
||||||
|
currentBoutNumber: currentQueueBoutNumber,
|
||||||
|
currentMatchId: matchId || null,
|
||||||
|
liveMatchData: {},
|
||||||
|
lastMatchResult: "",
|
||||||
|
state: null,
|
||||||
|
finished: false,
|
||||||
|
timerBannerState: null,
|
||||||
|
previousTimerSnapshot: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectedBoutStorageKey(tournamentId, matId) {
|
||||||
|
return `mat-selected-bout:${tournamentId}:${matId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchStorageKey(tournamentId, boutNumber) {
|
||||||
|
if (!boutNumber) return null
|
||||||
|
return buildStorageKey(tournamentId, boutNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractLiveMatchData(data) {
|
||||||
|
const extracted = {}
|
||||||
|
if (data.w1_stat !== undefined) extracted.w1_stat = data.w1_stat
|
||||||
|
if (data.w2_stat !== undefined) extracted.w2_stat = data.w2_stat
|
||||||
|
if (data.score !== undefined) extracted.score = data.score
|
||||||
|
if (data.win_type !== undefined) extracted.win_type = data.win_type
|
||||||
|
if (data.winner_name !== undefined) extracted.winner_name = data.winner_name
|
||||||
|
if (data.finished !== undefined) extracted.finished = data.finished
|
||||||
|
return extracted
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyStatePayloadContext(currentContext, payload) {
|
||||||
|
return {
|
||||||
|
...currentContext,
|
||||||
|
state: payload,
|
||||||
|
finished: Boolean(payload?.matchResult?.finished),
|
||||||
|
currentBoutNumber: payload?.metadata?.boutNumber || currentContext.currentBoutNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyMatchPayloadContext(currentContext, data) {
|
||||||
|
const nextContext = { ...currentContext }
|
||||||
|
|
||||||
|
if (data.scoreboard_state) {
|
||||||
|
Object.assign(nextContext, applyStatePayloadContext(nextContext, data.scoreboard_state))
|
||||||
|
}
|
||||||
|
|
||||||
|
nextContext.liveMatchData = {
|
||||||
|
...currentContext.liveMatchData,
|
||||||
|
...extractLiveMatchData(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.finished !== undefined) {
|
||||||
|
nextContext.finished = Boolean(data.finished)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextContext
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyMatPayloadContext(currentContext, data) {
|
||||||
|
const currentQueueBoutNumber = data.queue1_bout_number || null
|
||||||
|
const lastMatchResult = data.last_match_result || ""
|
||||||
|
|
||||||
|
if (currentContext.sourceMode === "localstorage") {
|
||||||
|
return {
|
||||||
|
...currentContext,
|
||||||
|
currentQueueBoutNumber: data.selected_bout_number || currentQueueBoutNumber,
|
||||||
|
lastMatchResult,
|
||||||
|
loadSelectedBout: true,
|
||||||
|
loadLocalState: true,
|
||||||
|
unsubscribeMatch: false,
|
||||||
|
subscribeMatchId: null,
|
||||||
|
renderNow: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextMatchId = data.selected_match_id || data.queue1_match_id || null
|
||||||
|
const nextBoutNumber = data.selected_bout_number || data.queue1_bout_number || null
|
||||||
|
const matchChanged = nextMatchId !== currentContext.currentMatchId
|
||||||
|
|
||||||
|
if (!nextMatchId) {
|
||||||
|
return {
|
||||||
|
...currentContext,
|
||||||
|
currentQueueBoutNumber,
|
||||||
|
lastMatchResult,
|
||||||
|
currentMatchId: null,
|
||||||
|
currentBoutNumber: nextBoutNumber,
|
||||||
|
state: null,
|
||||||
|
liveMatchData: {},
|
||||||
|
resetTimerBanner: true,
|
||||||
|
unsubscribeMatch: true,
|
||||||
|
subscribeMatchId: null,
|
||||||
|
renderNow: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...currentContext,
|
||||||
|
currentQueueBoutNumber,
|
||||||
|
lastMatchResult,
|
||||||
|
currentMatchId: nextMatchId,
|
||||||
|
currentBoutNumber: nextBoutNumber,
|
||||||
|
state: matchChanged ? null : currentContext.state,
|
||||||
|
liveMatchData: matchChanged ? {} : currentContext.liveMatchData,
|
||||||
|
resetTimerBanner: matchChanged,
|
||||||
|
unsubscribeMatch: false,
|
||||||
|
subscribeMatchId: matchChanged ? nextMatchId : null,
|
||||||
|
renderNow: matchChanged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function connectionPlan(sourceMode, currentMatchId) {
|
||||||
|
return {
|
||||||
|
useStorageListener: sourceMode === "localstorage",
|
||||||
|
subscribeMat: sourceMode === "localstorage" || sourceMode === "mat_websocket",
|
||||||
|
subscribeMatch: sourceMode === "mat_websocket" || sourceMode === "websocket",
|
||||||
|
matchId: sourceMode === "mat_websocket" || sourceMode === "websocket" ? currentMatchId : null,
|
||||||
|
loadSelectedBout: sourceMode === "localstorage",
|
||||||
|
loadLocalState: sourceMode === "localstorage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function storageChangePlan(currentContext, eventKey, tournamentId, matId) {
|
||||||
|
const selectedKey = selectedBoutStorageKey(tournamentId, matId)
|
||||||
|
if (eventKey === selectedKey) {
|
||||||
|
return {
|
||||||
|
loadSelectedBout: true,
|
||||||
|
loadLocalState: true,
|
||||||
|
renderNow: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const storageKey = matchStorageKey(tournamentId, currentContext.currentBoutNumber)
|
||||||
|
if (!storageKey || eventKey !== storageKey) {
|
||||||
|
return {
|
||||||
|
loadSelectedBout: false,
|
||||||
|
loadLocalState: false,
|
||||||
|
renderNow: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadSelectedBout: false,
|
||||||
|
loadLocalState: true,
|
||||||
|
renderNow: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectedBoutNumber(selection, currentQueueBoutNumber) {
|
||||||
|
return selection?.boutNumber || currentQueueBoutNumber
|
||||||
|
}
|
||||||
66
app/assets/javascripts/lib/match_state/serializers.js
Normal file
66
app/assets/javascripts/lib/match_state/serializers.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { buildInitialState } from "match-state-engine"
|
||||||
|
|
||||||
|
export function buildMatchMetadata(values) {
|
||||||
|
return {
|
||||||
|
tournamentId: values.tournamentId,
|
||||||
|
boutNumber: values.boutNumber,
|
||||||
|
weightLabel: values.weightLabel,
|
||||||
|
ruleset: values.ruleset,
|
||||||
|
bracketPosition: values.bracketPosition,
|
||||||
|
w1Name: values.w1Name,
|
||||||
|
w2Name: values.w2Name,
|
||||||
|
w1School: values.w1School,
|
||||||
|
w2School: values.w2School
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildStorageKey(tournamentId, boutNumber) {
|
||||||
|
return `match-state:${tournamentId}:${boutNumber}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPersistedState(state, metadata) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restorePersistedState(config, parsedState) {
|
||||||
|
const initialState = buildInitialState(config)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...initialState,
|
||||||
|
...parsedState,
|
||||||
|
participantScores: {
|
||||||
|
...initialState.participantScores,
|
||||||
|
...(parsedState.participantScores || {})
|
||||||
|
},
|
||||||
|
assignment: {
|
||||||
|
...initialState.assignment,
|
||||||
|
...(parsedState.assignment || {})
|
||||||
|
},
|
||||||
|
clock: {
|
||||||
|
...initialState.clock,
|
||||||
|
...(parsedState.clock || {})
|
||||||
|
},
|
||||||
|
timers: {
|
||||||
|
w1: {
|
||||||
|
...initialState.timers.w1,
|
||||||
|
...(parsedState.timers?.w1 || {})
|
||||||
|
},
|
||||||
|
w2: {
|
||||||
|
...initialState.timers.w2,
|
||||||
|
...(parsedState.timers?.w2 || {})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clocksByPhase: Object.fromEntries(
|
||||||
|
Object.entries(initialState.clocksByPhase).map(([phaseKey, defaultClock]) => [
|
||||||
|
phaseKey,
|
||||||
|
{
|
||||||
|
...defaultClock,
|
||||||
|
...(parsedState.clocksByPhase?.[phaseKey] || {})
|
||||||
|
}
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
39
app/assets/javascripts/lib/match_state/transport.js
Normal file
39
app/assets/javascripts/lib/match_state/transport.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
export function loadJson(storage, key) {
|
||||||
|
try {
|
||||||
|
const rawValue = storage.getItem(key)
|
||||||
|
if (!rawValue) return null
|
||||||
|
return JSON.parse(rawValue)
|
||||||
|
} catch (_error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveJson(storage, key, value) {
|
||||||
|
try {
|
||||||
|
storage.setItem(key, JSON.stringify(value))
|
||||||
|
return true
|
||||||
|
} catch (_error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeKey(storage, key) {
|
||||||
|
try {
|
||||||
|
storage.removeItem(key)
|
||||||
|
return true
|
||||||
|
} catch (_error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function performIfChanged(subscription, action, payload, lastSerializedPayload) {
|
||||||
|
if (!subscription) return lastSerializedPayload
|
||||||
|
|
||||||
|
const serializedPayload = JSON.stringify(payload)
|
||||||
|
if (serializedPayload === lastSerializedPayload) {
|
||||||
|
return lastSerializedPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription.perform(action, payload)
|
||||||
|
return serializedPayload
|
||||||
|
}
|
||||||
15
app/channels/mat_scoreboard_channel.rb
Normal file
15
app/channels/mat_scoreboard_channel.rb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
class MatScoreboardChannel < ApplicationCable::Channel
|
||||||
|
def subscribed
|
||||||
|
@mat = Mat.find_by(id: params[:mat_id])
|
||||||
|
return reject unless @mat
|
||||||
|
|
||||||
|
stream_for @mat
|
||||||
|
transmit(scoreboard_payload(@mat))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def scoreboard_payload(mat)
|
||||||
|
mat.scoreboard_payload
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
class MatchChannel < ApplicationCable::Channel
|
class MatchChannel < ApplicationCable::Channel
|
||||||
|
SCOREBOARD_CACHE_TTL = 1.hours
|
||||||
|
|
||||||
def subscribed
|
def subscribed
|
||||||
@match = Match.find_by(id: params[:match_id])
|
@match = Match.find_by(id: params[:match_id])
|
||||||
Rails.logger.info "[MatchChannel] Client subscribed with match_id: #{params[:match_id]}. Match found: #{@match.present?}"
|
Rails.logger.info "[MatchChannel] Client subscribed with match_id: #{params[:match_id]}. Match found: #{@match.present?}"
|
||||||
@@ -11,6 +13,19 @@ class MatchChannel < ApplicationCable::Channel
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def send_scoreboard(data)
|
||||||
|
unless @match
|
||||||
|
Rails.logger.error "[MatchChannel] Error: send_scoreboard called but @match is nil. Client params on sub: #{params[:match_id]}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
scoreboard_state = data["scoreboard_state"]
|
||||||
|
return if scoreboard_state.blank?
|
||||||
|
|
||||||
|
Rails.cache.write(scoreboard_cache_key, scoreboard_state, expires_in: SCOREBOARD_CACHE_TTL)
|
||||||
|
MatchChannel.broadcast_to(@match, { scoreboard_state: scoreboard_state })
|
||||||
|
end
|
||||||
|
|
||||||
def unsubscribed
|
def unsubscribed
|
||||||
Rails.logger.info "[MatchChannel] Client unsubscribed for match #{@match&.id}"
|
Rails.logger.info "[MatchChannel] Client unsubscribed for match #{@match&.id}"
|
||||||
end
|
end
|
||||||
@@ -75,7 +90,8 @@ class MatchChannel < ApplicationCable::Channel
|
|||||||
win_type: @match.win_type,
|
win_type: @match.win_type,
|
||||||
winner_name: @match.winner&.name,
|
winner_name: @match.winner&.name,
|
||||||
winner_id: @match.winner_id,
|
winner_id: @match.winner_id,
|
||||||
finished: @match.finished
|
finished: @match.finished,
|
||||||
|
scoreboard_state: Rails.cache.read(scoreboard_cache_key)
|
||||||
}.compact
|
}.compact
|
||||||
|
|
||||||
if payload.present?
|
if payload.present?
|
||||||
@@ -85,4 +101,10 @@ class MatchChannel < ApplicationCable::Channel
|
|||||||
Rails.logger.info "[MatchChannel] request_sync payload empty for match #{@match.id}, not transmitting."
|
Rails.logger.info "[MatchChannel] request_sync payload empty for match #{@match.id}, not transmitting."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def scoreboard_cache_key
|
||||||
|
"tournament:#{@match.tournament_id}:match:#{@match.id}:scoreboard_state"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class MatAssignmentRulesController < ApplicationController
|
|||||||
before_action :set_mat_assignment_rule, only: [:edit, :update, :destroy]
|
before_action :set_mat_assignment_rule, only: [:edit, :update, :destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@mat_assignment_rules = @tournament.mat_assignment_rules
|
@mat_assignment_rules = @tournament.mat_assignment_rules.includes(:mat)
|
||||||
@weights_by_id = @tournament.weights.index_by(&:id) # For quick lookup
|
@weights_by_id = @tournament.weights.index_by(&:id) # For quick lookup
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class MatchesController < ApplicationController
|
class MatchesController < ApplicationController
|
||||||
before_action :set_match, only: [:show, :edit, :update, :stat, :spectate, :edit_assignment, :update_assignment]
|
before_action :set_match, only: [:show, :edit, :update, :stat, :state, :spectate, :edit_assignment, :update_assignment]
|
||||||
before_action :check_access, only: [:edit, :update, :stat, :edit_assignment, :update_assignment]
|
before_action :check_access, only: [:edit, :update, :stat, :state, :edit_assignment, :update_assignment]
|
||||||
|
before_action :check_read_access, only: [:spectate]
|
||||||
|
|
||||||
# GET /matches/1
|
# GET /matches/1
|
||||||
# GET /matches/1.json
|
# GET /matches/1.json
|
||||||
@@ -22,49 +23,12 @@ class MatchesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def stat
|
def stat
|
||||||
# @show_next_bout_button = false
|
load_match_stat_context
|
||||||
if params[:match]
|
end
|
||||||
@match = Match.where(:id => params[:match]).includes(:wrestlers).first
|
|
||||||
end
|
def state
|
||||||
@wrestlers = []
|
load_match_stat_context
|
||||||
if @match
|
@match_state_ruleset = "folkstyle_usa"
|
||||||
if @match.w1
|
|
||||||
@wrestler1_name = @match.wrestler1.name
|
|
||||||
@wrestler1_school_name = @match.wrestler1.school.name
|
|
||||||
@wrestler1_last_match = @match.wrestler1.last_match
|
|
||||||
@wrestlers.push(@match.wrestler1)
|
|
||||||
else
|
|
||||||
@wrestler1_name = "Not assigned"
|
|
||||||
@wrestler1_school_name = "N/A"
|
|
||||||
@wrestler1_last_match = nil
|
|
||||||
end
|
|
||||||
if @match.w2
|
|
||||||
@wrestler2_name = @match.wrestler2.name
|
|
||||||
@wrestler2_school_name = @match.wrestler2.school.name
|
|
||||||
@wrestler2_last_match = @match.wrestler2.last_match
|
|
||||||
@wrestlers.push(@match.wrestler2)
|
|
||||||
else
|
|
||||||
@wrestler2_name = "Not assigned"
|
|
||||||
@wrestler2_school_name = "N/A"
|
|
||||||
@wrestler2_last_match = nil
|
|
||||||
end
|
|
||||||
@tournament = @match.tournament
|
|
||||||
end
|
|
||||||
if @match&.mat
|
|
||||||
@mat = @match.mat
|
|
||||||
queue_position = @mat.queue_position_for_match(@match)
|
|
||||||
@next_match = queue_position == 1 ? @mat.queue2_match : nil
|
|
||||||
@show_next_bout_button = queue_position == 1
|
|
||||||
if request.referer&.include?("/tournaments/#{@tournament.id}/matches")
|
|
||||||
session[:return_path] = "/tournaments/#{@tournament.id}/matches"
|
|
||||||
else
|
|
||||||
session[:return_path] = mat_path(@mat)
|
|
||||||
end
|
|
||||||
session[:error_return_path] = "/matches/#{@match.id}/stat"
|
|
||||||
else
|
|
||||||
session[:return_path] = "/tournaments/#{@tournament.id}/matches"
|
|
||||||
session[:error_return_path] = "/matches/#{@match.id}/stat"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /matches/:id/spectate
|
# GET /matches/:id/spectate
|
||||||
@@ -142,26 +106,19 @@ class MatchesController < ApplicationController
|
|||||||
win_type: @match.win_type,
|
win_type: @match.win_type,
|
||||||
winner_id: @match.winner_id,
|
winner_id: @match.winner_id,
|
||||||
winner_name: @match.winner&.name,
|
winner_name: @match.winner&.name,
|
||||||
finished: @match.finished
|
finished: @match.finished,
|
||||||
|
scoreboard_state: Rails.cache.read("tournament:#{@match.tournament_id}:match:#{@match.id}:scoreboard_state")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if session[:return_path]
|
redirect_path = resolve_match_redirect_path(session[:return_path]) || "/tournaments/#{@match.tournament.id}"
|
||||||
sanitized_return_path = sanitize_return_path(session[:return_path])
|
format.html { redirect_to redirect_path, notice: 'Match was successfully updated.' }
|
||||||
format.html { redirect_to sanitized_return_path, notice: 'Match was successfully updated.' }
|
session.delete(:return_path)
|
||||||
session.delete(:return_path) # Remove the session variable
|
|
||||||
else
|
|
||||||
format.html { redirect_to "/tournaments/#{@match.tournament.id}", notice: 'Match was successfully updated.' }
|
|
||||||
end
|
|
||||||
format.json { head :no_content }
|
format.json { head :no_content }
|
||||||
else
|
else
|
||||||
if session[:error_return_path]
|
error_path = resolve_match_redirect_path(session[:error_return_path]) || "/tournaments/#{@match.tournament.id}"
|
||||||
format.html { redirect_to session.delete(:error_return_path), alert: "Match did not save because: #{@match.errors.full_messages.to_s}" }
|
format.html { redirect_to error_path, alert: "Match did not save because: #{@match.errors.full_messages.to_s}" }
|
||||||
format.json { render json: @match.errors, status: :unprocessable_entity }
|
format.json { render json: @match.errors, status: :unprocessable_entity }
|
||||||
else
|
|
||||||
format.html { redirect_to "/tournaments/#{@match.tournament.id}", alert: "Match did not save because: #{@match.errors.full_messages.to_s}" }
|
|
||||||
format.json { render json: @match.errors, status: :unprocessable_entity }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -182,11 +139,66 @@ class MatchesController < ApplicationController
|
|||||||
authorize! :manage, @match.tournament
|
authorize! :manage, @match.tournament
|
||||||
end
|
end
|
||||||
|
|
||||||
def sanitize_return_path(path)
|
def check_read_access
|
||||||
|
authorize! :read, @match.tournament
|
||||||
|
end
|
||||||
|
|
||||||
|
def sanitize_redirect_path(path)
|
||||||
|
return nil if path.blank?
|
||||||
|
|
||||||
uri = URI.parse(path)
|
uri = URI.parse(path)
|
||||||
params = Rack::Utils.parse_nested_query(uri.query)
|
return nil if uri.scheme.present? || uri.host.present?
|
||||||
params.delete("bout_number") # Remove the bout_number param
|
|
||||||
uri.query = params.to_query.presence # Rebuild the query string or set it to nil if empty
|
uri.to_s
|
||||||
uri.to_s # Return the full path as a string
|
rescue URI::InvalidURIError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolve_match_redirect_path(fallback_path)
|
||||||
|
sanitize_redirect_path(params[:redirect_to].presence) || sanitize_redirect_path(fallback_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_match_stat_context
|
||||||
|
if params[:match]
|
||||||
|
@match = Match.where(:id => params[:match]).includes(:wrestlers).first
|
||||||
|
end
|
||||||
|
|
||||||
|
@wrestlers = []
|
||||||
|
if @match
|
||||||
|
if @match.w1
|
||||||
|
@wrestler1_name = @match.wrestler1.name
|
||||||
|
@wrestler1_school_name = @match.wrestler1.school.name
|
||||||
|
@wrestler1_last_match = @match.wrestler1.last_match
|
||||||
|
@wrestlers.push(@match.wrestler1)
|
||||||
|
else
|
||||||
|
@wrestler1_name = "Not assigned"
|
||||||
|
@wrestler1_school_name = "N/A"
|
||||||
|
@wrestler1_last_match = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if @match.w2
|
||||||
|
@wrestler2_name = @match.wrestler2.name
|
||||||
|
@wrestler2_school_name = @match.wrestler2.school.name
|
||||||
|
@wrestler2_last_match = @match.wrestler2.last_match
|
||||||
|
@wrestlers.push(@match.wrestler2)
|
||||||
|
else
|
||||||
|
@wrestler2_name = "Not assigned"
|
||||||
|
@wrestler2_school_name = "N/A"
|
||||||
|
@wrestler2_last_match = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
@tournament = @match.tournament
|
||||||
|
end
|
||||||
|
|
||||||
|
if @match&.mat
|
||||||
|
@mat = @match.mat
|
||||||
|
queue_position = @mat.queue_position_for_match(@match)
|
||||||
|
@next_match = queue_position == 1 ? @mat.queue2_match : nil
|
||||||
|
@show_next_bout_button = queue_position == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
@match_results_redirect_path = sanitize_redirect_path(params[:redirect_to].presence) || "/tournaments/#{@tournament.id}/matches"
|
||||||
|
session[:return_path] = @match_results_redirect_path
|
||||||
|
session[:error_return_path] = request.original_fullpath
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
class MatsController < ApplicationController
|
class MatsController < ApplicationController
|
||||||
before_action :set_mat, only: [:show, :edit, :update, :destroy, :assign_next_match]
|
before_action :set_mat, only: [:show, :state, :scoreboard, :edit, :update, :destroy, :assign_next_match, :select_match]
|
||||||
before_action :check_access, only: [:new,:create,:update,:destroy,:edit,:show, :assign_next_match]
|
before_action :check_access, only: [:new,:create,:update,:destroy,:edit,:show, :state, :scoreboard, :assign_next_match, :select_match]
|
||||||
before_action :check_for_matches, only: [:show]
|
|
||||||
|
|
||||||
# GET /mats/1
|
# GET /mats/1
|
||||||
# GET /mats/1.json
|
# GET /mats/1.json
|
||||||
def show
|
def show
|
||||||
bout_number_param = params[:bout_number] # Read the bout_number from the URL params
|
bout_number_param = params[:bout_number]
|
||||||
|
@queue_matches = @mat.queue_matches
|
||||||
if bout_number_param
|
@match = if bout_number_param
|
||||||
@show_next_bout_button = false
|
@queue_matches.compact.find { |m| m.bout_number == bout_number_param.to_i }
|
||||||
@match = @mat.queue_matches.compact.find { |m| m.bout_number == bout_number_param.to_i }
|
|
||||||
else
|
else
|
||||||
@show_next_bout_button = true
|
@queue_matches[0]
|
||||||
@match = @mat.queue1_match
|
|
||||||
end
|
end
|
||||||
|
# If a requested bout is no longer queued, fall back to queue1.
|
||||||
@next_match = @mat.queue2_match # Second match on the mat
|
@match ||= @queue_matches[0]
|
||||||
|
@next_match = @queue_matches[1]
|
||||||
|
@show_next_bout_button = false
|
||||||
|
|
||||||
@wrestlers = []
|
@wrestlers = []
|
||||||
if @match
|
if @match
|
||||||
@@ -45,10 +44,33 @@ class MatsController < ApplicationController
|
|||||||
@tournament = @match.tournament
|
@tournament = @match.tournament
|
||||||
end
|
end
|
||||||
|
|
||||||
session[:return_path] = request.original_fullpath
|
@match_results_redirect_path = sanitize_mat_redirect_path(params[:redirect_to].presence || request.original_fullpath)
|
||||||
|
session[:return_path] = @match_results_redirect_path
|
||||||
session[:error_return_path] = request.original_fullpath
|
session[:error_return_path] = request.original_fullpath
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def scoreboard
|
||||||
|
@match = @mat.selected_scoreboard_match || @mat.queue1_match
|
||||||
|
@tournament = @mat.tournament
|
||||||
|
end
|
||||||
|
|
||||||
|
def state
|
||||||
|
load_mat_match_context
|
||||||
|
@match_state_ruleset = "folkstyle_usa"
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_match
|
||||||
|
selected_match = @mat.queue_matches.compact.find do |match|
|
||||||
|
match.id == params[:match_id].to_i || match.bout_number == params[:bout_number].to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
return head :unprocessable_entity unless selected_match || params[:last_match_result].present?
|
||||||
|
|
||||||
|
@mat.set_selected_scoreboard_match!(selected_match) if selected_match
|
||||||
|
@mat.set_last_match_result!(params[:last_match_result]) if params.key?(:last_match_result)
|
||||||
|
head :no_content
|
||||||
|
end
|
||||||
|
|
||||||
# GET /mats/new
|
# GET /mats/new
|
||||||
def new
|
def new
|
||||||
@mat = Mat.new
|
@mat = Mat.new
|
||||||
@@ -141,12 +163,65 @@ class MatsController < ApplicationController
|
|||||||
authorize! :manage, @tournament
|
authorize! :manage, @tournament
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sanitize_mat_redirect_path(path)
|
||||||
|
return nil if path.blank?
|
||||||
|
|
||||||
def check_for_matches
|
uri = URI.parse(path)
|
||||||
if @mat
|
return nil if uri.scheme.present? || uri.host.present?
|
||||||
if @mat.tournament.matches.empty?
|
|
||||||
redirect_to "/tournaments/#{@tournament.id}/no_matches"
|
params = Rack::Utils.parse_nested_query(uri.query)
|
||||||
end
|
params.delete("bout_number")
|
||||||
|
uri.query = params.to_query.presence
|
||||||
|
uri.to_s
|
||||||
|
rescue URI::InvalidURIError
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
def load_mat_match_context
|
||||||
|
bout_number_param = params[:bout_number]
|
||||||
|
@queue_matches = @mat.queue_matches
|
||||||
|
@match = if bout_number_param
|
||||||
|
@queue_matches.compact.find { |match| match.bout_number == bout_number_param.to_i }
|
||||||
|
else
|
||||||
|
@queue_matches[0]
|
||||||
|
end
|
||||||
|
@match ||= @queue_matches[0]
|
||||||
|
@next_match = @queue_matches[1]
|
||||||
|
@show_next_bout_button = false
|
||||||
|
|
||||||
|
@wrestlers = []
|
||||||
|
if @match
|
||||||
|
if @match.w1
|
||||||
|
@wrestler1_name = @match.wrestler1.name
|
||||||
|
@wrestler1_school_name = @match.wrestler1.school.name
|
||||||
|
@wrestler1_last_match = @match.wrestler1.last_match
|
||||||
|
@wrestlers.push(@match.wrestler1)
|
||||||
|
else
|
||||||
|
@wrestler1_name = "Not assigned"
|
||||||
|
@wrestler1_school_name = "N/A"
|
||||||
|
@wrestler1_last_match = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if @match.w2
|
||||||
|
@wrestler2_name = @match.wrestler2.name
|
||||||
|
@wrestler2_school_name = @match.wrestler2.school.name
|
||||||
|
@wrestler2_last_match = @match.wrestler2.last_match
|
||||||
|
@wrestlers.push(@match.wrestler2)
|
||||||
|
else
|
||||||
|
@wrestler2_name = "Not assigned"
|
||||||
|
@wrestler2_school_name = "N/A"
|
||||||
|
@wrestler2_last_match = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
@tournament = @match.tournament
|
||||||
|
else
|
||||||
|
@tournament = @mat.tournament
|
||||||
|
end
|
||||||
|
|
||||||
|
@match_results_redirect_path = sanitize_mat_redirect_path(params[:redirect_to].presence || request.original_fullpath)
|
||||||
|
session[:return_path] = @match_results_redirect_path
|
||||||
|
session[:error_return_path] = request.original_fullpath
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
class StaticPagesController < ApplicationController
|
class StaticPagesController < ApplicationController
|
||||||
|
|
||||||
def my_tournaments
|
def my_tournaments
|
||||||
tournaments_created = current_user.tournaments
|
tournaments_created = current_user.tournaments.to_a
|
||||||
tournaments_delegated = current_user.delegated_tournaments
|
tournaments_delegated = current_user.delegated_tournaments.to_a
|
||||||
all_tournaments = tournaments_created + tournaments_delegated
|
all_tournaments = tournaments_created + tournaments_delegated
|
||||||
@tournaments = all_tournaments.sort_by{|t| t.days_until_start}
|
@tournaments = all_tournaments.sort_by{|t| t.days_until_start}
|
||||||
@schools = current_user.delegated_schools
|
@schools = current_user.delegated_schools.includes(:tournament)
|
||||||
end
|
end
|
||||||
|
|
||||||
def not_allowed
|
def not_allowed
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
class TournamentsController < ApplicationController
|
class TournamentsController < ApplicationController
|
||||||
before_action :set_tournament, only: [:all_results, :delete_school_keys, :generate_school_keys,:reset_bout_board,:calculate_team_scores,:bout_sheets,:swap,:weigh_in_sheet,:error,:teampointadjust,:remove_teampointadjust,:remove_school_delegate,:remove_delegate,:school_delegate,:delegate,:matches,:weigh_in,:weigh_in_weight,:create_custom_weights,:show,:edit,:update,:destroy,:up_matches,:no_matches,:team_scores,:generate_matches,:bracket,:all_brackets,:qrcode]
|
before_action :set_tournament, only: [:all_results, :delete_school_keys, :generate_school_keys,:reset_bout_board,:calculate_team_scores,:bout_sheets,:swap,:weigh_in_sheet,:error,:teampointadjust,:remove_teampointadjust,:remove_school_delegate,:remove_delegate,:school_delegate,:delegate,:matches,:weigh_in,:weigh_in_weight,:create_custom_weights,:show,:edit,:update,:destroy,:up_matches,:no_matches,:team_scores,:generate_matches,:bracket,:all_brackets,:qrcode,:live_scores]
|
||||||
before_action :check_access_manage, only: [:delete_school_keys, :generate_school_keys,:reset_bout_board,:calculate_team_scores,:swap,:weigh_in_sheet,:teampointadjust,:remove_teampointadjust,:remove_school_delegate,:school_delegate,:weigh_in,:weigh_in_weight,:create_custom_weights,:update,:edit,:generate_matches,:matches,:qrcode]
|
before_action :check_access_manage, only: [:delete_school_keys, :generate_school_keys,:reset_bout_board,:calculate_team_scores,:swap,:weigh_in_sheet,:teampointadjust,:remove_teampointadjust,:remove_school_delegate,:school_delegate,:weigh_in,:weigh_in_weight,:create_custom_weights,:update,:edit,:generate_matches,:matches,:qrcode]
|
||||||
before_action :check_access_destroy, only: [:destroy,:delegate,:remove_delegate]
|
before_action :check_access_destroy, only: [:destroy,:delegate,:remove_delegate]
|
||||||
before_action :check_tournament_errors, only: [:generate_matches]
|
before_action :check_tournament_errors, only: [:generate_matches]
|
||||||
before_action :check_for_matches, only: [:all_results,:up_matches,:bracket,:all_brackets]
|
before_action :check_for_matches, only: [:all_results,:bracket,:all_brackets]
|
||||||
before_action :check_access_read, only: [:all_results,:up_matches,:bracket,:all_brackets]
|
before_action :check_access_read, only: [:all_results,:up_matches,:bracket,:all_brackets,:live_scores]
|
||||||
|
|
||||||
def weigh_in_sheet
|
def weigh_in_sheet
|
||||||
|
@schools = @tournament.schools.includes(wrestlers: :weight)
|
||||||
end
|
end
|
||||||
|
|
||||||
def calculate_team_scores
|
def calculate_team_scores
|
||||||
@@ -92,12 +92,9 @@ class TournamentsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@users_delegates = []
|
@users_delegates = SchoolDelegate.includes(:user, :school)
|
||||||
@tournament.schools.each do |s|
|
.joins(:school)
|
||||||
s.delegates.each do |d|
|
.where(schools: { tournament_id: @tournament.id })
|
||||||
@users_delegates << d
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def delegate
|
def delegate
|
||||||
@@ -115,11 +112,63 @@ class TournamentsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@users_delegates = @tournament.delegates
|
@users_delegates = @tournament.delegates.includes(:user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def matches
|
def matches
|
||||||
@matches = @tournament.matches.includes(:wrestlers,:schools).sort_by{|m| m.bout_number}
|
per_page = 50
|
||||||
|
@page = params[:page].to_i > 0 ? params[:page].to_i : 1
|
||||||
|
offset = (@page - 1) * per_page
|
||||||
|
matches_table = Match.arel_table
|
||||||
|
|
||||||
|
matches_scope = @tournament.matches.order(:bout_number)
|
||||||
|
|
||||||
|
if params[:search].present?
|
||||||
|
wrestlers_table = Wrestler.arel_table
|
||||||
|
schools_table = School.arel_table
|
||||||
|
search_terms = params[:search].downcase.split
|
||||||
|
|
||||||
|
search_terms.each do |term|
|
||||||
|
escaped_term = ActiveRecord::Base.sanitize_sql_like(term)
|
||||||
|
pattern = "%#{escaped_term}%"
|
||||||
|
|
||||||
|
matching_wrestler_ids = Wrestler
|
||||||
|
.joins(:weight)
|
||||||
|
.left_outer_joins(:school)
|
||||||
|
.where(weights: { tournament_id: @tournament.id })
|
||||||
|
.where(
|
||||||
|
wrestlers_table[:name].matches(pattern)
|
||||||
|
.or(schools_table[:name].matches(pattern))
|
||||||
|
)
|
||||||
|
.distinct
|
||||||
|
.select(:id)
|
||||||
|
|
||||||
|
term_scope = @tournament.matches.where(w1: matching_wrestler_ids)
|
||||||
|
.or(@tournament.matches.where(w2: matching_wrestler_ids))
|
||||||
|
|
||||||
|
if term.match?(/\A\d+\z/)
|
||||||
|
term_scope = term_scope.or(@tournament.matches.where(bout_number: term.to_i))
|
||||||
|
end
|
||||||
|
|
||||||
|
matches_scope = matches_scope.where(id: term_scope.select(:id))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@total_count = matches_scope.count
|
||||||
|
@total_pages = (@total_count / per_page.to_f).ceil
|
||||||
|
@per_page = per_page
|
||||||
|
|
||||||
|
loser1_not_bye = matches_table[:loser1_name].not_eq("BYE").or(matches_table[:loser1_name].eq(nil))
|
||||||
|
loser2_not_bye = matches_table[:loser2_name].not_eq("BYE").or(matches_table[:loser2_name].eq(nil))
|
||||||
|
|
||||||
|
non_bye_scope = matches_scope.where(loser1_not_bye).where(loser2_not_bye)
|
||||||
|
@matches_without_byes_count = non_bye_scope.count
|
||||||
|
@unfinished_matches_without_byes_count = non_bye_scope.where(finished: [nil, 0]).count
|
||||||
|
|
||||||
|
@matches = matches_scope
|
||||||
|
.includes({ wrestler1: :school }, { wrestler2: :school }, { weight: :matches })
|
||||||
|
.offset(offset)
|
||||||
|
.limit(per_page)
|
||||||
if @match
|
if @match
|
||||||
@w1 = @match.wrestler1
|
@w1 = @match.wrestler1
|
||||||
@w2 = @match.wrestler2
|
@w2 = @match.wrestler2
|
||||||
@@ -129,10 +178,18 @@ class TournamentsController < ApplicationController
|
|||||||
|
|
||||||
def weigh_in_weight
|
def weigh_in_weight
|
||||||
if params[:wrestler]
|
if params[:wrestler]
|
||||||
Wrestler.update(params[:wrestler].keys, params[:wrestler].values)
|
sanitized_wrestlers = params.require(:wrestler).to_unsafe_h.each_with_object({}) do |(wrestler_id, attributes), result|
|
||||||
|
permitted = ActionController::Parameters.new(attributes).permit(:offical_weight)
|
||||||
|
result[wrestler_id] = permitted
|
||||||
|
end
|
||||||
|
Wrestler.update(sanitized_wrestlers.keys, sanitized_wrestlers.values) if sanitized_wrestlers.present?
|
||||||
|
redirect_to "/tournaments/#{@tournament.id}/weigh_in/#{params[:weight]}", notice: "Weights were successfully recorded."
|
||||||
|
return
|
||||||
end
|
end
|
||||||
if params[:weight]
|
if params[:weight]
|
||||||
@weight = Weight.where(:id => params[:weight]).includes(:wrestlers).first
|
@weight = Weight.where(id: params[:weight])
|
||||||
|
.includes(wrestlers: [:school, :weight])
|
||||||
|
.first
|
||||||
@tournament_id = @tournament.id
|
@tournament_id = @tournament.id
|
||||||
@tournament_name = @tournament.name
|
@tournament_name = @tournament.name
|
||||||
@weights = @tournament.weights
|
@weights = @tournament.weights
|
||||||
@@ -159,8 +216,11 @@ class TournamentsController < ApplicationController
|
|||||||
def all_brackets
|
def all_brackets
|
||||||
@schools = @tournament.schools
|
@schools = @tournament.schools
|
||||||
@schools = @schools.sort_by{|s| s.page_score_string}.reverse!
|
@schools = @schools.sort_by{|s| s.page_score_string}.reverse!
|
||||||
@matches = @tournament.matches.includes(:wrestlers,:schools)
|
@weights = @tournament.weights.includes(:matches, wrestlers: :school)
|
||||||
@weights = @tournament.weights.includes(:matches,:wrestlers)
|
all_matches = @tournament.matches.includes(:weight, { wrestler1: :school }, { wrestler2: :school })
|
||||||
|
all_wrestlers = @tournament.wrestlers.includes(:school, :weight)
|
||||||
|
@matches_by_weight_id = all_matches.group_by(&:weight_id)
|
||||||
|
@wrestlers_by_weight_id = all_wrestlers.group_by(&:weight_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def bracket
|
def bracket
|
||||||
@@ -182,6 +242,10 @@ class TournamentsController < ApplicationController
|
|||||||
@bracket_position = nil
|
@bracket_position = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def live_scores
|
||||||
|
@mats = @tournament.mats.sort_by(&:name)
|
||||||
|
end
|
||||||
|
|
||||||
def generate_matches
|
def generate_matches
|
||||||
GenerateTournamentMatches.new(@tournament).generate
|
GenerateTournamentMatches.new(@tournament).generate
|
||||||
end
|
end
|
||||||
@@ -203,25 +267,30 @@ class TournamentsController < ApplicationController
|
|||||||
|
|
||||||
|
|
||||||
def up_matches
|
def up_matches
|
||||||
# .where.not(loser1_name: 'BYE') won't return matches with NULL loser1_name
|
@matches = @tournament.up_matches_unassigned_matches
|
||||||
# so I was only getting back matches with Loser of BOUT_NUMBER
|
@mats = @tournament.up_matches_mats
|
||||||
@matches = @tournament.matches
|
|
||||||
.where("mat_id is NULL and (finished != 1 or finished is NULL)")
|
|
||||||
.where("loser1_name != ? OR loser1_name IS NULL", "BYE")
|
|
||||||
.where("loser2_name != ? OR loser2_name IS NULL", "BYE")
|
|
||||||
.order('bout_number ASC')
|
|
||||||
.limit(10).includes(:wrestlers)
|
|
||||||
@mats = @tournament.mats.includes(:matches)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def bout_sheets
|
def bout_sheets
|
||||||
|
matches_scope = @tournament.matches
|
||||||
|
.where("loser1_name != ? OR loser1_name IS NULL", "BYE")
|
||||||
|
.where("loser2_name != ? OR loser2_name IS NULL", "BYE")
|
||||||
|
|
||||||
if params[:round]
|
if params[:round]
|
||||||
round = params[:round]
|
round = params[:round]
|
||||||
if round != "All"
|
if round != "All"
|
||||||
@matches = @tournament.matches.where("round = ?",round).sort_by{|match| match.bout_number}
|
@matches = matches_scope
|
||||||
|
.where(round: round)
|
||||||
|
.includes(:weight)
|
||||||
|
.order(:bout_number)
|
||||||
else
|
else
|
||||||
@matches = @tournament.matches.sort_by{|match| match.bout_number}
|
@matches = matches_scope
|
||||||
|
.includes(:weight)
|
||||||
|
.order(:bout_number)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
wrestler_ids = @matches.flat_map { |match| [match.w1, match.w2] }.compact.uniq
|
||||||
|
@wrestlers_by_id = Wrestler.includes(:school).where(id: wrestler_ids).index_by(&:id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ class Mat < ApplicationRecord
|
|||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
|
|
||||||
QUEUE_SLOTS = %w[queue1 queue2 queue3 queue4].freeze
|
QUEUE_SLOTS = %w[queue1 queue2 queue3 queue4].freeze
|
||||||
|
SCOREBOARD_SELECTION_CACHE_TTL = 1.hours
|
||||||
|
LAST_MATCH_RESULT_CACHE_TTL = 1.hours
|
||||||
|
|
||||||
after_save :clear_queue_matches_cache
|
after_save :clear_queue_matches_cache
|
||||||
|
after_commit :broadcast_up_matches_board, on: :update, if: :up_matches_queue_changed?
|
||||||
|
|
||||||
def assign_next_match
|
def assign_next_match
|
||||||
slot = first_empty_queue_slot
|
slot = first_empty_queue_slot
|
||||||
@@ -96,7 +99,9 @@ class Mat < ApplicationRecord
|
|||||||
@queue_matches = if ids.empty?
|
@queue_matches = if ids.empty?
|
||||||
[nil, nil, nil, nil]
|
[nil, nil, nil, nil]
|
||||||
else
|
else
|
||||||
matches_by_id = Match.where(id: ids).index_by(&:id)
|
matches_by_id = Match.where(id: ids)
|
||||||
|
.includes({ wrestler1: :school }, { wrestler2: :school }, { weight: :matches })
|
||||||
|
.index_by(&:id)
|
||||||
slot_ids.map { |match_id| match_id ? matches_by_id[match_id] : nil }
|
slot_ids.map { |match_id| match_id ? matches_by_id[match_id] : nil }
|
||||||
end
|
end
|
||||||
@queue_match_slot_ids = slot_ids
|
@queue_match_slot_ids = slot_ids
|
||||||
@@ -181,12 +186,63 @@ class Mat < ApplicationRecord
|
|||||||
|
|
||||||
def clear_queue!
|
def clear_queue!
|
||||||
update!(queue1: nil, queue2: nil, queue3: nil, queue4: nil)
|
update!(queue1: nil, queue2: nil, queue3: nil, queue4: nil)
|
||||||
|
broadcast_current_match
|
||||||
end
|
end
|
||||||
|
|
||||||
def unfinished_matches
|
def unfinished_matches
|
||||||
matches.select{|m| m.finished != 1}.sort_by{|m| m.bout_number}
|
matches.select{|m| m.finished != 1}.sort_by{|m| m.bout_number}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def scoreboard_payload
|
||||||
|
selected_match = selected_scoreboard_match
|
||||||
|
{
|
||||||
|
mat_id: id,
|
||||||
|
queue1_bout_number: queue1_match&.bout_number,
|
||||||
|
queue1_match_id: queue1_match&.id,
|
||||||
|
selected_bout_number: selected_match&.bout_number,
|
||||||
|
selected_match_id: selected_match&.id,
|
||||||
|
last_match_result: last_match_result_text
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_selected_scoreboard_match!(match)
|
||||||
|
if match
|
||||||
|
Rails.cache.write(
|
||||||
|
scoreboard_selection_cache_key,
|
||||||
|
{ match_id: match.id, bout_number: match.bout_number },
|
||||||
|
expires_in: SCOREBOARD_SELECTION_CACHE_TTL
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Rails.cache.delete(scoreboard_selection_cache_key)
|
||||||
|
end
|
||||||
|
broadcast_current_match
|
||||||
|
end
|
||||||
|
|
||||||
|
def selected_scoreboard_match
|
||||||
|
selection = Rails.cache.read(scoreboard_selection_cache_key)
|
||||||
|
return nil unless selection
|
||||||
|
|
||||||
|
match_id = selection[:match_id] || selection["match_id"]
|
||||||
|
selected_match = queue_matches.compact.find { |match| match.id == match_id }
|
||||||
|
return selected_match if selected_match
|
||||||
|
|
||||||
|
Rails.cache.delete(scoreboard_selection_cache_key)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_last_match_result!(text)
|
||||||
|
if text.present?
|
||||||
|
Rails.cache.write(last_match_result_cache_key, text, expires_in: LAST_MATCH_RESULT_CACHE_TTL)
|
||||||
|
else
|
||||||
|
Rails.cache.delete(last_match_result_cache_key)
|
||||||
|
end
|
||||||
|
broadcast_current_match
|
||||||
|
end
|
||||||
|
|
||||||
|
def last_match_result_text
|
||||||
|
Rails.cache.read(last_match_result_cache_key)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def clear_queue_matches_cache
|
def clear_queue_matches_cache
|
||||||
@@ -272,6 +328,23 @@ class Mat < ApplicationRecord
|
|||||||
show_next_bout_button: true
|
show_next_bout_button: true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
MatScoreboardChannel.broadcast_to(self, scoreboard_payload)
|
||||||
|
end
|
||||||
|
|
||||||
|
def scoreboard_selection_cache_key
|
||||||
|
"tournament:#{tournament_id}:mat:#{id}:scoreboard_selection"
|
||||||
|
end
|
||||||
|
|
||||||
|
def last_match_result_cache_key
|
||||||
|
"tournament:#{tournament_id}:mat:#{id}:last_match_result"
|
||||||
|
end
|
||||||
|
|
||||||
|
def broadcast_up_matches_board
|
||||||
|
Tournament.broadcast_up_matches_board(tournament_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def up_matches_queue_changed?
|
||||||
|
saved_change_to_queue1? || saved_change_to_queue2? || saved_change_to_queue3? || saved_change_to_queue4?
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ class Match < ApplicationRecord
|
|||||||
belongs_to :weight, touch: true
|
belongs_to :weight, touch: true
|
||||||
belongs_to :mat, touch: true, optional: true
|
belongs_to :mat, touch: true, optional: true
|
||||||
belongs_to :winner, class_name: 'Wrestler', foreign_key: 'winner_id', optional: true
|
belongs_to :winner, class_name: 'Wrestler', foreign_key: 'winner_id', optional: true
|
||||||
|
belongs_to :wrestler1, class_name: 'Wrestler', foreign_key: 'w1', optional: true
|
||||||
|
belongs_to :wrestler2, class_name: 'Wrestler', foreign_key: 'w2', optional: true
|
||||||
has_many :wrestlers, :through => :weight
|
has_many :wrestlers, :through => :weight
|
||||||
has_many :schools, :through => :wrestlers
|
has_many :schools, :through => :wrestlers
|
||||||
validate :score_validation, :win_type_validation, :bracket_position_validation, :overtime_type_validation
|
validate :score_validation, :win_type_validation, :bracket_position_validation, :overtime_type_validation
|
||||||
@@ -15,6 +17,7 @@ class Match < ApplicationRecord
|
|||||||
# update mat show with correct match if bout board is reset
|
# update mat show with correct match if bout board is reset
|
||||||
# this is done with a turbo stream
|
# this is done with a turbo stream
|
||||||
after_commit :broadcast_mat_assignment_change, if: :saved_change_to_mat_id?, on: [:create, :update]
|
after_commit :broadcast_mat_assignment_change, if: :saved_change_to_mat_id?, on: [:create, :update]
|
||||||
|
after_commit :broadcast_up_matches_board, on: :update, if: :saved_change_to_mat_id?
|
||||||
|
|
||||||
# Enqueue advancement and related actions after the DB transaction has committed.
|
# Enqueue advancement and related actions after the DB transaction has committed.
|
||||||
# Using after_commit ensures any background jobs enqueued inside these callbacks
|
# Using after_commit ensures any background jobs enqueued inside these callbacks
|
||||||
@@ -178,14 +181,6 @@ class Match < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def wrestler1
|
|
||||||
wrestlers.select{|w| w.id == self.w1}.first
|
|
||||||
end
|
|
||||||
|
|
||||||
def wrestler2
|
|
||||||
wrestlers.select{|w| w.id == self.w2}.first
|
|
||||||
end
|
|
||||||
|
|
||||||
def w1_name
|
def w1_name
|
||||||
if self.w1 != nil
|
if self.w1 != nil
|
||||||
wrestler1.name
|
wrestler1.name
|
||||||
@@ -203,7 +198,7 @@ class Match < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def w1_bracket_name
|
def w1_bracket_name
|
||||||
first_round = self.weight.matches.sort_by{|m| m.round}.first.round
|
first_round = first_round_for_weight
|
||||||
return_string = ""
|
return_string = ""
|
||||||
return_string_ending = ""
|
return_string_ending = ""
|
||||||
if self.w1 and self.winner_id == self.w1
|
if self.w1 and self.winner_id == self.w1
|
||||||
@@ -223,7 +218,7 @@ class Match < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def w2_bracket_name
|
def w2_bracket_name
|
||||||
first_round = self.weight.matches.sort_by{|m| m.round}.first.round
|
first_round = first_round_for_weight
|
||||||
return_string = ""
|
return_string = ""
|
||||||
return_string_ending = ""
|
return_string_ending = ""
|
||||||
if self.w2 and self.winner_id == self.w2
|
if self.w2 and self.winner_id == self.w2
|
||||||
@@ -289,6 +284,17 @@ class Match < ApplicationRecord
|
|||||||
self.weight.max
|
self.weight.max
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def first_round_for_weight
|
||||||
|
return @first_round_for_weight if defined?(@first_round_for_weight)
|
||||||
|
|
||||||
|
@first_round_for_weight =
|
||||||
|
if association(:weight).loaded? && self.weight&.association(:matches)&.loaded?
|
||||||
|
self.weight.matches.map(&:round).compact.min
|
||||||
|
else
|
||||||
|
Match.where(weight_id: self.weight_id).minimum(:round)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def replace_loser_name_with_wrestler(w,loser_name)
|
def replace_loser_name_with_wrestler(w,loser_name)
|
||||||
if self.loser1_name == loser_name
|
if self.loser1_name == loser_name
|
||||||
self.w1 = w.id
|
self.w1 = w.id
|
||||||
@@ -366,4 +372,8 @@ class Match < ApplicationRecord
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def broadcast_up_matches_board
|
||||||
|
Tournament.broadcast_up_matches_board(tournament_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -69,8 +69,35 @@ class Tournament < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def up_matches_unassigned_matches
|
||||||
|
matches
|
||||||
|
.where("mat_id is NULL and (finished != 1 or finished is NULL)")
|
||||||
|
.where("loser1_name != ? OR loser1_name IS NULL", "BYE")
|
||||||
|
.where("loser2_name != ? OR loser2_name IS NULL", "BYE")
|
||||||
|
.order("bout_number ASC")
|
||||||
|
.limit(10)
|
||||||
|
.includes({ wrestler1: :school }, { wrestler2: :school }, { weight: :matches })
|
||||||
|
end
|
||||||
|
|
||||||
|
def up_matches_mats
|
||||||
|
mats.includes(:matches)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.broadcast_up_matches_board(tournament_id)
|
||||||
|
tournament = find_by(id: tournament_id)
|
||||||
|
return unless tournament
|
||||||
|
|
||||||
|
Turbo::StreamsChannel.broadcast_replace_to(
|
||||||
|
tournament,
|
||||||
|
target: "up_matches_board",
|
||||||
|
partial: "tournaments/up_matches_board",
|
||||||
|
locals: { tournament: tournament }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def destroy_all_matches
|
def destroy_all_matches
|
||||||
matches.destroy_all
|
matches.destroy_all
|
||||||
|
mats.each(&:clear_queue!)
|
||||||
end
|
end
|
||||||
|
|
||||||
def matches_by_round(round)
|
def matches_by_round(round)
|
||||||
@@ -97,18 +124,11 @@ class Tournament < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def pointAdjustments
|
def pointAdjustments
|
||||||
point_adjustments = []
|
school_scope = Teampointadjust.where(school_id: schools.select(:id))
|
||||||
self.schools.each do |s|
|
wrestler_scope = Teampointadjust.where(wrestler_id: wrestlers.select(:id))
|
||||||
s.deductedPoints.each do |d|
|
|
||||||
point_adjustments << d
|
Teampointadjust.includes(:school, :wrestler)
|
||||||
end
|
.merge(school_scope.or(wrestler_scope))
|
||||||
end
|
|
||||||
self.wrestlers.each do |w|
|
|
||||||
w.deductedPoints.each do |d|
|
|
||||||
point_adjustments << d
|
|
||||||
end
|
|
||||||
end
|
|
||||||
point_adjustments
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_school_delegations
|
def remove_school_delegations
|
||||||
|
|||||||
@@ -53,19 +53,16 @@ class User < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def delegated_tournaments
|
def delegated_tournaments
|
||||||
tournaments_delegated = []
|
Tournament.joins(:delegates)
|
||||||
delegated_tournament_permissions.each do |t|
|
.where(tournament_delegates: { user_id: id })
|
||||||
tournaments_delegated << t.tournament
|
.distinct
|
||||||
end
|
|
||||||
tournaments_delegated
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def delegated_schools
|
def delegated_schools
|
||||||
schools_delegated = []
|
School.joins(:delegates)
|
||||||
delegated_school_permissions.each do |t|
|
.where(school_delegates: { user_id: id })
|
||||||
schools_delegated << t.school
|
.includes(:tournament)
|
||||||
end
|
.distinct
|
||||||
schools_delegated
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.search(search)
|
def self.search(search)
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ class Weight < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def calculate_bracket_size
|
def calculate_bracket_size
|
||||||
num_wrestlers = wrestlers.reload.size
|
num_wrestlers = wrestlers.size
|
||||||
return nil if num_wrestlers <= 0 # Handle invalid input
|
return nil if num_wrestlers <= 0 # Handle invalid input
|
||||||
|
|
||||||
# Find the smallest power of 2 greater than or equal to num_wrestlers
|
# Find the smallest power of 2 greater than or equal to num_wrestlers
|
||||||
|
|||||||
@@ -12,21 +12,97 @@ class AdvanceWrestler
|
|||||||
end
|
end
|
||||||
|
|
||||||
def advance_raw
|
def advance_raw
|
||||||
@last_match.reload
|
@last_match = Match.find_by(id: @last_match&.id)
|
||||||
@wrestler.reload
|
@wrestler = Wrestler.includes(:school, :weight).find_by(id: @wrestler.id)
|
||||||
if @last_match && @last_match.finished?
|
return unless @last_match && @wrestler && @last_match.finished?
|
||||||
pool_to_bracket_advancement if @tournament.tournament_type == "Pool to bracket"
|
|
||||||
ModifiedDoubleEliminationAdvance.new(@wrestler, @last_match).bracket_advancement if @tournament.tournament_type.include? "Modified 16 Man Double Elimination"
|
context = preload_advancement_context
|
||||||
DoubleEliminationAdvance.new(@wrestler, @last_match).bracket_advancement if @tournament.tournament_type.include? "Regular Double Elimination"
|
matches_to_advance = []
|
||||||
|
|
||||||
|
if @tournament.tournament_type == "Pool to bracket"
|
||||||
|
matches_to_advance.concat(pool_to_bracket_advancement(context))
|
||||||
|
elsif @tournament.tournament_type.include?("Modified 16 Man Double Elimination")
|
||||||
|
service = ModifiedDoubleEliminationAdvance.new(@wrestler, @last_match, matches: context[:matches])
|
||||||
|
service.bracket_advancement
|
||||||
|
matches_to_advance.concat(service.matches_to_advance)
|
||||||
|
elsif @tournament.tournament_type.include?("Regular Double Elimination")
|
||||||
|
service = DoubleEliminationAdvance.new(@wrestler, @last_match, matches: context[:matches])
|
||||||
|
service.bracket_advancement
|
||||||
|
matches_to_advance.concat(service.matches_to_advance)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
persist_advancement_changes(context)
|
||||||
|
advance_pending_matches(matches_to_advance)
|
||||||
@wrestler.school.calculate_score
|
@wrestler.school.calculate_score
|
||||||
end
|
end
|
||||||
|
|
||||||
def pool_to_bracket_advancement
|
def preload_advancement_context
|
||||||
if @wrestler.weight.all_pool_matches_finished(@wrestler.pool) and (@wrestler.finished_bracket_matches.size < 1)
|
weight = Weight.includes(:matches, :wrestlers).find(@wrestler.weight_id)
|
||||||
PoolOrder.new(@wrestler.weight.wrestlers_in_pool(@wrestler.pool)).getPoolOrder
|
{
|
||||||
|
weight: weight,
|
||||||
|
matches: weight.matches.to_a,
|
||||||
|
wrestlers: weight.wrestlers.to_a
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def persist_advancement_changes(context)
|
||||||
|
persist_matches(context[:matches])
|
||||||
|
persist_wrestlers(context[:wrestlers])
|
||||||
|
end
|
||||||
|
|
||||||
|
def persist_matches(matches)
|
||||||
|
timestamp = Time.current
|
||||||
|
updates = matches.filter_map do |m|
|
||||||
|
next unless m.changed?
|
||||||
|
|
||||||
|
{
|
||||||
|
id: m.id,
|
||||||
|
w1: m.w1,
|
||||||
|
w2: m.w2,
|
||||||
|
winner_id: m.winner_id,
|
||||||
|
win_type: m.win_type,
|
||||||
|
score: m.score,
|
||||||
|
finished: m.finished,
|
||||||
|
loser1_name: m.loser1_name,
|
||||||
|
loser2_name: m.loser2_name,
|
||||||
|
finished_at: m.finished_at,
|
||||||
|
updated_at: timestamp
|
||||||
|
}
|
||||||
end
|
end
|
||||||
PoolAdvance.new(@wrestler).advanceWrestler
|
Match.upsert_all(updates) if updates.any?
|
||||||
|
end
|
||||||
|
|
||||||
|
def persist_wrestlers(wrestlers)
|
||||||
|
timestamp = Time.current
|
||||||
|
updates = wrestlers.filter_map do |w|
|
||||||
|
next unless w.changed?
|
||||||
|
|
||||||
|
{
|
||||||
|
id: w.id,
|
||||||
|
pool_placement: w.pool_placement,
|
||||||
|
pool_placement_tiebreaker: w.pool_placement_tiebreaker,
|
||||||
|
updated_at: timestamp
|
||||||
|
}
|
||||||
|
end
|
||||||
|
Wrestler.upsert_all(updates) if updates.any?
|
||||||
|
end
|
||||||
|
|
||||||
|
def advance_pending_matches(matches_to_advance)
|
||||||
|
matches_to_advance.uniq(&:id).each do |match|
|
||||||
|
match.advance_wrestlers
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def pool_to_bracket_advancement(context)
|
||||||
|
matches_to_advance = []
|
||||||
|
wrestlers_in_pool = context[:wrestlers].select { |w| w.pool == @wrestler.pool }
|
||||||
|
if @wrestler.weight.all_pool_matches_finished(@wrestler.pool) && (@wrestler.finished_bracket_matches.size < 1)
|
||||||
|
PoolOrder.new(wrestlers_in_pool).getPoolOrder
|
||||||
|
end
|
||||||
|
service = PoolAdvance.new(@wrestler, @last_match, matches: context[:matches], wrestlers: context[:wrestlers])
|
||||||
|
service.advanceWrestler
|
||||||
|
matches_to_advance.concat(service.matches_to_advance)
|
||||||
|
matches_to_advance
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
class DoubleEliminationAdvance
|
class DoubleEliminationAdvance
|
||||||
|
|
||||||
def initialize(wrestler,last_match)
|
attr_reader :matches_to_advance
|
||||||
|
|
||||||
|
def initialize(wrestler,last_match, matches: nil)
|
||||||
@wrestler = wrestler
|
@wrestler = wrestler
|
||||||
@last_match = last_match
|
@last_match = last_match
|
||||||
|
@matches = matches || @wrestler.weight.matches.to_a
|
||||||
|
@matches_to_advance = []
|
||||||
@next_match_position_number = (@last_match.bracket_position_number / 2.0)
|
@next_match_position_number = (@last_match.bracket_position_number / 2.0)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -48,7 +52,7 @@ class DoubleEliminationAdvance
|
|||||||
end
|
end
|
||||||
|
|
||||||
if next_match_bracket_position
|
if next_match_bracket_position
|
||||||
next_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?",next_match_bracket_position,next_match_position_number.ceil,@wrestler.weight_id).first
|
next_match = @matches.find { |m| m.bracket_position == next_match_bracket_position && m.bracket_position_number == next_match_position_number.ceil && m.weight_id == @wrestler.weight_id }
|
||||||
end
|
end
|
||||||
|
|
||||||
if next_match
|
if next_match
|
||||||
@@ -59,18 +63,16 @@ class DoubleEliminationAdvance
|
|||||||
def update_new_match(match, wrestler_number)
|
def update_new_match(match, wrestler_number)
|
||||||
if wrestler_number == 2 or (match.loser1_name and match.loser1_name.include? "Loser of")
|
if wrestler_number == 2 or (match.loser1_name and match.loser1_name.include? "Loser of")
|
||||||
match.w2 = @wrestler.id
|
match.w2 = @wrestler.id
|
||||||
match.save
|
|
||||||
elsif wrestler_number == 1
|
elsif wrestler_number == 1
|
||||||
match.w1 = @wrestler.id
|
match.w1 = @wrestler.id
|
||||||
match.save
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_consolation_bye
|
def update_consolation_bye
|
||||||
bout = @wrestler.last_match.bout_number
|
bout = @wrestler.last_match.bout_number
|
||||||
next_match = Match.where("(loser1_name = ? OR loser2_name = ?) AND weight_id = ?","Loser of #{bout}","Loser of #{bout}",@wrestler.weight_id)
|
next_match = @matches.find { |m| m.weight_id == @wrestler.weight_id && (m.loser1_name == "Loser of #{bout}" || m.loser2_name == "Loser of #{bout}") }
|
||||||
if next_match.size > 0
|
if next_match
|
||||||
next_match.first.replace_loser_name_with_bye("Loser of #{bout}")
|
replace_loser_name_with_bye(next_match, "Loser of #{bout}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -84,27 +86,18 @@ class DoubleEliminationAdvance
|
|||||||
|
|
||||||
def losers_bracket_advancement
|
def losers_bracket_advancement
|
||||||
bout = @last_match.bout_number
|
bout = @last_match.bout_number
|
||||||
next_match = Match.where("(loser1_name = ? OR loser2_name = ?) AND weight_id = ?", "Loser of #{bout}", "Loser of #{bout}", @wrestler.weight_id).first
|
next_match = @matches.find { |m| m.weight_id == @wrestler.weight_id && (m.loser1_name == "Loser of #{bout}" || m.loser2_name == "Loser of #{bout}") }
|
||||||
|
|
||||||
if next_match.present?
|
if next_match.present?
|
||||||
next_match.replace_loser_name_with_wrestler(@wrestler, "Loser of #{bout}")
|
replace_loser_name_with_wrestler(next_match, @wrestler, "Loser of #{bout}")
|
||||||
next_match.reload
|
|
||||||
|
|
||||||
if next_match.loser1_name == "BYE" || next_match.loser2_name == "BYE"
|
if next_match.loser1_name == "BYE" || next_match.loser2_name == "BYE"
|
||||||
next_match.winner_id = @wrestler.id
|
next_match.winner_id = @wrestler.id
|
||||||
next_match.win_type = "BYE"
|
next_match.win_type = "BYE"
|
||||||
next_match.score = ""
|
next_match.score = ""
|
||||||
next_match.finished = 1
|
next_match.finished = 1
|
||||||
# puts "Before save: winner_id=#{next_match.winner_id}"
|
next_match.finished_at = Time.current
|
||||||
|
@matches_to_advance << next_match
|
||||||
# if next_match.save
|
|
||||||
# puts "Save successful: winner_id=#{next_match.reload.winner_id}"
|
|
||||||
# else
|
|
||||||
# puts "Save failed: #{next_match.errors.full_messages}"
|
|
||||||
# end
|
|
||||||
next_match.save
|
|
||||||
|
|
||||||
next_match.advance_wrestlers
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -112,51 +105,69 @@ class DoubleEliminationAdvance
|
|||||||
|
|
||||||
def advance_double_byes
|
def advance_double_byes
|
||||||
weight = @wrestler.weight
|
weight = @wrestler.weight
|
||||||
weight.matches.select{|m| m.loser1_name == "BYE" and m.loser2_name == "BYE" and m.finished != 1}.each do |match|
|
@matches.select{|m| m.weight_id == weight.id && m.loser1_name == "BYE" and m.loser2_name == "BYE" and m.finished != 1}.each do |match|
|
||||||
match.finished = 1
|
match.finished = 1
|
||||||
|
match.finished_at = Time.current
|
||||||
match.score = ""
|
match.score = ""
|
||||||
match.win_type = "BYE"
|
match.win_type = "BYE"
|
||||||
next_match_position_number = (match.bracket_position_number / 2.0).ceil
|
next_match_position_number = (match.bracket_position_number / 2.0).ceil
|
||||||
after_matches = weight.matches.select{|m| m.round > match.round and m.is_consolation_match == match.is_consolation_match }.sort_by{|m| m.round}
|
after_matches = @matches.select{|m| m.weight_id == weight.id && m.round > match.round and m.is_consolation_match == match.is_consolation_match }.sort_by{|m| m.round}
|
||||||
next_matches = weight.matches.select{|m| m.round == after_matches.first.round and m.is_consolation_match == match.is_consolation_match }
|
next if after_matches.empty?
|
||||||
this_round_matches = weight.matches.select{|m| m.round == match.round and m.is_consolation_match == match.is_consolation_match }
|
next_matches = @matches.select{|m| m.weight_id == weight.id && m.round == after_matches.first.round and m.is_consolation_match == match.is_consolation_match }
|
||||||
|
this_round_matches = @matches.select{|m| m.weight_id == weight.id && m.round == match.round and m.is_consolation_match == match.is_consolation_match }
|
||||||
|
next_match = nil
|
||||||
|
|
||||||
if next_matches.size == this_round_matches.size
|
if next_matches.size == this_round_matches.size
|
||||||
next_match = next_matches.select{|m| m.bracket_position_number == match.bracket_position_number}.first
|
next_match = next_matches.select{|m| m.bracket_position_number == match.bracket_position_number}.first
|
||||||
next_match.loser2_name = "BYE"
|
next_match.loser2_name = "BYE" if next_match
|
||||||
next_match.save
|
|
||||||
elsif next_matches.size < this_round_matches.size and next_matches.size > 0
|
elsif next_matches.size < this_round_matches.size and next_matches.size > 0
|
||||||
next_match = next_matches.select{|m| m.bracket_position_number == next_match_position_number}.first
|
next_match = next_matches.select{|m| m.bracket_position_number == next_match_position_number}.first
|
||||||
if next_match.bracket_position_number == next_match_position_number
|
if next_match && next_match.bracket_position_number == next_match_position_number
|
||||||
next_match.loser2_name = "BYE"
|
next_match.loser2_name = "BYE"
|
||||||
else
|
elsif next_match
|
||||||
next_match.loser1_name = "BYE"
|
next_match.loser1_name = "BYE"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
next_match.save
|
|
||||||
match.save
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_bye_for_placement
|
def set_bye_for_placement
|
||||||
weight = @wrestler.weight
|
weight = @wrestler.weight
|
||||||
fifth_finals = weight.matches.select{|match| match.bracket_position == '5/6'}.first
|
fifth_finals = @matches.select{|match| match.weight_id == weight.id && match.bracket_position == '5/6'}.first
|
||||||
seventh_finals = weight.matches.select{|match| match.bracket_position == '7/8'}.first
|
seventh_finals = @matches.select{|match| match.weight_id == weight.id && match.bracket_position == '7/8'}.first
|
||||||
if seventh_finals
|
if seventh_finals
|
||||||
conso_quarter = weight.matches.select{|match| match.bracket_position == 'Conso Quarter'}
|
conso_quarter = @matches.select{|match| match.weight_id == weight.id && match.bracket_position == 'Conso Quarter'}
|
||||||
conso_quarter.each do |match|
|
conso_quarter.each do |match|
|
||||||
if match.loser1_name == "BYE" or match.loser2_name == "BYE"
|
if match.loser1_name == "BYE" or match.loser2_name == "BYE"
|
||||||
seventh_finals.replace_loser_name_with_bye("Loser of #{match.bout_number}")
|
replace_loser_name_with_bye(seventh_finals, "Loser of #{match.bout_number}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if fifth_finals
|
if fifth_finals
|
||||||
conso_semis = weight.matches.select{|match| match.bracket_position == 'Conso Semis'}
|
conso_semis = @matches.select{|match| match.weight_id == weight.id && match.bracket_position == 'Conso Semis'}
|
||||||
conso_semis.each do |match|
|
conso_semis.each do |match|
|
||||||
if match.loser1_name == "BYE" or match.loser2_name == "BYE"
|
if match.loser1_name == "BYE" or match.loser2_name == "BYE"
|
||||||
fifth_finals.replace_loser_name_with_bye("Loser of #{match.bout_number}")
|
replace_loser_name_with_bye(fifth_finals, "Loser of #{match.bout_number}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def replace_loser_name_with_wrestler(match, wrestler, loser_name)
|
||||||
|
if match.loser1_name == loser_name
|
||||||
|
match.w1 = wrestler.id
|
||||||
|
end
|
||||||
|
if match.loser2_name == loser_name
|
||||||
|
match.w2 = wrestler.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def replace_loser_name_with_bye(match, loser_name)
|
||||||
|
if match.loser1_name == loser_name
|
||||||
|
match.loser1_name = "BYE"
|
||||||
|
end
|
||||||
|
if match.loser2_name == loser_name
|
||||||
|
match.loser2_name = "BYE"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
class ModifiedDoubleEliminationAdvance
|
class ModifiedDoubleEliminationAdvance
|
||||||
|
|
||||||
def initialize(wrestler,last_match)
|
attr_reader :matches_to_advance
|
||||||
|
|
||||||
|
def initialize(wrestler,last_match, matches: nil)
|
||||||
@wrestler = wrestler
|
@wrestler = wrestler
|
||||||
@last_match = last_match
|
@last_match = last_match
|
||||||
|
@matches = matches || @wrestler.weight.matches.to_a
|
||||||
|
@matches_to_advance = []
|
||||||
@next_match_position_number = (@last_match.bracket_position_number / 2.0)
|
@next_match_position_number = (@last_match.bracket_position_number / 2.0)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -25,42 +29,41 @@ class ModifiedDoubleEliminationAdvance
|
|||||||
update_consolation_bye
|
update_consolation_bye
|
||||||
end
|
end
|
||||||
if @last_match.bracket_position == "Quarter"
|
if @last_match.bracket_position == "Quarter"
|
||||||
new_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","Semis",@next_match_position_number.ceil,@wrestler.weight_id).first
|
new_match = @matches.find { |m| m.bracket_position == "Semis" && m.bracket_position_number == @next_match_position_number.ceil && m.weight_id == @wrestler.weight_id }
|
||||||
update_new_match(new_match, get_wrestler_number)
|
update_new_match(new_match, get_wrestler_number)
|
||||||
elsif @last_match.bracket_position == "Semis"
|
elsif @last_match.bracket_position == "Semis"
|
||||||
new_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","1/2",@next_match_position_number.ceil,@wrestler.weight_id).first
|
new_match = @matches.find { |m| m.bracket_position == "1/2" && m.bracket_position_number == @next_match_position_number.ceil && m.weight_id == @wrestler.weight_id }
|
||||||
update_new_match(new_match, get_wrestler_number)
|
update_new_match(new_match, get_wrestler_number)
|
||||||
elsif @last_match.bracket_position == "Conso Semis"
|
elsif @last_match.bracket_position == "Conso Semis"
|
||||||
new_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","5/6",@next_match_position_number.ceil,@wrestler.weight_id).first
|
new_match = @matches.find { |m| m.bracket_position == "5/6" && m.bracket_position_number == @next_match_position_number.ceil && m.weight_id == @wrestler.weight_id }
|
||||||
update_new_match(new_match, get_wrestler_number)
|
update_new_match(new_match, get_wrestler_number)
|
||||||
elsif @last_match.bracket_position == "Conso Quarter"
|
elsif @last_match.bracket_position == "Conso Quarter"
|
||||||
# it's a special bracket where a semi loser is not dropping down
|
# it's a special bracket where a semi loser is not dropping down
|
||||||
new_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","Conso Semis",@next_match_position_number.ceil,@wrestler.weight_id).first
|
new_match = @matches.find { |m| m.bracket_position == "Conso Semis" && m.bracket_position_number == @next_match_position_number.ceil && m.weight_id == @wrestler.weight_id }
|
||||||
update_new_match(new_match, get_wrestler_number)
|
update_new_match(new_match, get_wrestler_number)
|
||||||
elsif @last_match.bracket_position == "Bracket Round of 16"
|
elsif @last_match.bracket_position == "Bracket Round of 16"
|
||||||
new_match = Match.where("bracket_position_number = ? and weight_id = ? and round > ? and bracket_position = ?", @next_match_position_number.ceil,@wrestler.weight_id, @last_match.round , "Quarter").sort_by{|m| m.round}.first
|
new_match = @matches.select { |m| m.bracket_position_number == @next_match_position_number.ceil && m.weight_id == @wrestler.weight_id && m.round > @last_match.round && m.bracket_position == "Quarter" }.sort_by(&:round).first
|
||||||
update_new_match(new_match, get_wrestler_number)
|
update_new_match(new_match, get_wrestler_number)
|
||||||
elsif @last_match.bracket_position == "Conso Round of 8"
|
elsif @last_match.bracket_position == "Conso Round of 8"
|
||||||
new_match = Match.where("bracket_position_number = ? and weight_id = ? and round > ? and bracket_position = ?", @last_match.bracket_position_number,@wrestler.weight_id, @last_match.round, "Conso Quarter").sort_by{|m| m.round}.first
|
new_match = @matches.select { |m| m.bracket_position_number == @last_match.bracket_position_number && m.weight_id == @wrestler.weight_id && m.round > @last_match.round && m.bracket_position == "Conso Quarter" }.sort_by(&:round).first
|
||||||
update_new_match(new_match, get_wrestler_number)
|
update_new_match(new_match, get_wrestler_number)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_new_match(match, wrestler_number)
|
def update_new_match(match, wrestler_number)
|
||||||
|
return unless match
|
||||||
if wrestler_number == 2 or (match.loser1_name and match.loser1_name.include? "Loser of")
|
if wrestler_number == 2 or (match.loser1_name and match.loser1_name.include? "Loser of")
|
||||||
match.w2 = @wrestler.id
|
match.w2 = @wrestler.id
|
||||||
match.save
|
|
||||||
elsif wrestler_number == 1
|
elsif wrestler_number == 1
|
||||||
match.w1 = @wrestler.id
|
match.w1 = @wrestler.id
|
||||||
match.save
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_consolation_bye
|
def update_consolation_bye
|
||||||
bout = @wrestler.last_match.bout_number
|
bout = @wrestler.last_match.bout_number
|
||||||
next_match = Match.where("(loser1_name = ? OR loser2_name = ?) AND weight_id = ?","Loser of #{bout}","Loser of #{bout}",@wrestler.weight_id)
|
next_match = @matches.find { |m| m.weight_id == @wrestler.weight_id && (m.loser1_name == "Loser of #{bout}" || m.loser2_name == "Loser of #{bout}") }
|
||||||
if next_match.size > 0
|
if next_match
|
||||||
next_match.first.replace_loser_name_with_bye("Loser of #{bout}")
|
replace_loser_name_with_bye(next_match, "Loser of #{bout}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -74,27 +77,18 @@ class ModifiedDoubleEliminationAdvance
|
|||||||
|
|
||||||
def losers_bracket_advancement
|
def losers_bracket_advancement
|
||||||
bout = @last_match.bout_number
|
bout = @last_match.bout_number
|
||||||
next_match = Match.where("(loser1_name = ? OR loser2_name = ?) AND weight_id = ?", "Loser of #{bout}", "Loser of #{bout}", @wrestler.weight_id).first
|
next_match = @matches.find { |m| m.weight_id == @wrestler.weight_id && (m.loser1_name == "Loser of #{bout}" || m.loser2_name == "Loser of #{bout}") }
|
||||||
|
|
||||||
if next_match.present?
|
if next_match.present?
|
||||||
next_match.replace_loser_name_with_wrestler(@wrestler, "Loser of #{bout}")
|
replace_loser_name_with_wrestler(next_match, @wrestler, "Loser of #{bout}")
|
||||||
next_match.reload
|
|
||||||
|
|
||||||
if next_match.loser1_name == "BYE" || next_match.loser2_name == "BYE"
|
if next_match.loser1_name == "BYE" || next_match.loser2_name == "BYE"
|
||||||
next_match.winner_id = @wrestler.id
|
next_match.winner_id = @wrestler.id
|
||||||
next_match.win_type = "BYE"
|
next_match.win_type = "BYE"
|
||||||
next_match.score = ""
|
next_match.score = ""
|
||||||
next_match.finished = 1
|
next_match.finished = 1
|
||||||
# puts "Before save: winner_id=#{next_match.winner_id}"
|
next_match.finished_at = Time.current
|
||||||
|
@matches_to_advance << next_match
|
||||||
# if next_match.save
|
|
||||||
# puts "Save successful: winner_id=#{next_match.reload.winner_id}"
|
|
||||||
# else
|
|
||||||
# puts "Save failed: #{next_match.errors.full_messages}"
|
|
||||||
# end
|
|
||||||
next_match.save
|
|
||||||
|
|
||||||
next_match.advance_wrestlers
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -102,43 +96,53 @@ class ModifiedDoubleEliminationAdvance
|
|||||||
|
|
||||||
def advance_double_byes
|
def advance_double_byes
|
||||||
weight = @wrestler.weight
|
weight = @wrestler.weight
|
||||||
weight.matches.select{|m| m.loser1_name == "BYE" and m.loser2_name == "BYE" and m.finished != 1}.each do |match|
|
@matches.select{|m| m.weight_id == weight.id && m.loser1_name == "BYE" and m.loser2_name == "BYE" and m.finished != 1}.each do |match|
|
||||||
match.finished = 1
|
match.finished = 1
|
||||||
|
match.finished_at = Time.current
|
||||||
match.score = ""
|
match.score = ""
|
||||||
match.win_type = "BYE"
|
match.win_type = "BYE"
|
||||||
next_match_position_number = (match.bracket_position_number / 2.0).ceil
|
next_match_position_number = (match.bracket_position_number / 2.0).ceil
|
||||||
after_matches = weight.matches.select{|m| m.round > match.round and m.is_consolation_match == match.is_consolation_match }.sort_by{|m| m.round}
|
after_matches = @matches.select{|m| m.weight_id == weight.id && m.round > match.round and m.is_consolation_match == match.is_consolation_match }.sort_by{|m| m.round}
|
||||||
next_matches = weight.matches.select{|m| m.round == after_matches.first.round and m.is_consolation_match == match.is_consolation_match }
|
next if after_matches.empty?
|
||||||
this_round_matches = weight.matches.select{|m| m.round == match.round and m.is_consolation_match == match.is_consolation_match }
|
next_matches = @matches.select{|m| m.weight_id == weight.id && m.round == after_matches.first.round and m.is_consolation_match == match.is_consolation_match }
|
||||||
|
this_round_matches = @matches.select{|m| m.weight_id == weight.id && m.round == match.round and m.is_consolation_match == match.is_consolation_match }
|
||||||
|
next_match = nil
|
||||||
|
|
||||||
if next_matches.size == this_round_matches.size
|
if next_matches.size == this_round_matches.size
|
||||||
next_match = next_matches.select{|m| m.bracket_position_number == match.bracket_position_number}.first
|
next_match = next_matches.select{|m| m.bracket_position_number == match.bracket_position_number}.first
|
||||||
next_match.loser2_name = "BYE"
|
next_match.loser2_name = "BYE" if next_match
|
||||||
next_match.save
|
|
||||||
elsif next_matches.size < this_round_matches.size and next_matches.size > 0
|
elsif next_matches.size < this_round_matches.size and next_matches.size > 0
|
||||||
next_match = next_matches.select{|m| m.bracket_position_number == next_match_position_number}.first
|
next_match = next_matches.select{|m| m.bracket_position_number == next_match_position_number}.first
|
||||||
if next_match.bracket_position_number == next_match_position_number
|
if next_match && next_match.bracket_position_number == next_match_position_number
|
||||||
next_match.loser2_name = "BYE"
|
next_match.loser2_name = "BYE"
|
||||||
else
|
elsif next_match
|
||||||
next_match.loser1_name = "BYE"
|
next_match.loser1_name = "BYE"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
next_match.save
|
|
||||||
match.save
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_bye_for_placement
|
def set_bye_for_placement
|
||||||
weight = @wrestler.weight
|
weight = @wrestler.weight
|
||||||
seventh_finals = weight.matches.select{|match| match.bracket_position == '7/8'}.first
|
seventh_finals = @matches.select{|match| match.weight_id == weight.id && match.bracket_position == '7/8'}.first
|
||||||
if seventh_finals
|
if seventh_finals
|
||||||
conso_quarter = weight.matches.select{|match| match.bracket_position == 'Conso Semis'}
|
conso_quarter = @matches.select{|match| match.weight_id == weight.id && match.bracket_position == 'Conso Semis'}
|
||||||
conso_quarter.each do |match|
|
conso_quarter.each do |match|
|
||||||
if match.loser1_name == "BYE" or match.loser2_name == "BYE"
|
if match.loser1_name == "BYE" or match.loser2_name == "BYE"
|
||||||
seventh_finals.replace_loser_name_with_bye("Loser of #{match.bout_number}")
|
replace_loser_name_with_bye(seventh_finals, "Loser of #{match.bout_number}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def replace_loser_name_with_wrestler(match, wrestler, loser_name)
|
||||||
|
match.w1 = wrestler.id if match.loser1_name == loser_name
|
||||||
|
match.w2 = wrestler.id if match.loser2_name == loser_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def replace_loser_name_with_bye(match, loser_name)
|
||||||
|
match.loser1_name = "BYE" if match.loser1_name == loser_name
|
||||||
|
match.loser2_name = "BYE" if match.loser2_name == loser_name
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
class PoolAdvance
|
class PoolAdvance
|
||||||
|
|
||||||
def initialize(wrestler)
|
attr_reader :matches_to_advance
|
||||||
|
|
||||||
|
def initialize(wrestler, last_match, matches: nil, wrestlers: nil)
|
||||||
@wrestler = wrestler
|
@wrestler = wrestler
|
||||||
@last_match = @wrestler.last_match
|
@last_match = last_match
|
||||||
|
@matches = matches || @wrestler.weight.matches.to_a
|
||||||
|
@wrestlers = wrestlers || @wrestler.weight.wrestlers.to_a
|
||||||
|
@matches_to_advance = []
|
||||||
end
|
end
|
||||||
|
|
||||||
def advanceWrestler
|
def advanceWrestler
|
||||||
@@ -17,15 +22,15 @@ class PoolAdvance
|
|||||||
def poolToBracketAdvancment
|
def poolToBracketAdvancment
|
||||||
pool = @wrestler.pool
|
pool = @wrestler.pool
|
||||||
# This has to always run because the last match in a pool might not be a pool winner or runner up
|
# This has to always run because the last match in a pool might not be a pool winner or runner up
|
||||||
winner = Wrestler.where("weight_id = ? and pool_placement = 1 and pool = ?",@wrestler.weight.id, pool).first
|
winner = @wrestlers.find { |w| w.weight_id == @wrestler.weight.id && w.pool_placement == 1 && w.pool == pool }
|
||||||
runner_up = Wrestler.where("weight_id = ? and pool_placement = 2 and pool = ?",@wrestler.weight.id, pool).first
|
runner_up = @wrestlers.find { |w| w.weight_id == @wrestler.weight.id && w.pool_placement == 2 && w.pool == pool }
|
||||||
if runner_up
|
if runner_up
|
||||||
runner_up_match = Match.where("weight_id = ? and (loser1_name = ? or loser2_name = ?)",@wrestler.weight.id, "Runner Up Pool #{pool}", "Runner Up Pool #{pool}").first
|
runner_up_match = @matches.find { |m| m.weight_id == @wrestler.weight.id && (m.loser1_name == "Runner Up Pool #{pool}" || m.loser2_name == "Runner Up Pool #{pool}") }
|
||||||
runner_up_match.replace_loser_name_with_wrestler(runner_up,"Runner Up Pool #{pool}")
|
replace_loser_name_with_wrestler(runner_up_match, runner_up, "Runner Up Pool #{pool}") if runner_up_match
|
||||||
end
|
end
|
||||||
if winner
|
if winner
|
||||||
winner_match = Match.where("weight_id = ? and (loser1_name = ? or loser2_name = ?)",@wrestler.weight.id, "Winner Pool #{pool}", "Winner Pool #{pool}").first
|
winner_match = @matches.find { |m| m.weight_id == @wrestler.weight.id && (m.loser1_name == "Winner Pool #{pool}" || m.loser2_name == "Winner Pool #{pool}") }
|
||||||
winner_match.replace_loser_name_with_wrestler(winner,"Winner Pool #{pool}")
|
replace_loser_name_with_wrestler(winner_match, winner, "Winner Pool #{pool}") if winner_match
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -45,36 +50,40 @@ class PoolAdvance
|
|||||||
|
|
||||||
def winner_advance
|
def winner_advance
|
||||||
if @wrestler.last_match.bracket_position == "Quarter"
|
if @wrestler.last_match.bracket_position == "Quarter"
|
||||||
new_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","Semis",@wrestler.next_match_position_number.ceil,@wrestler.weight_id).first
|
new_match = @matches.find { |m| m.bracket_position == "Semis" && m.bracket_position_number == @wrestler.next_match_position_number.ceil && m.weight_id == @wrestler.weight_id }
|
||||||
updateNewMatch(new_match)
|
updateNewMatch(new_match)
|
||||||
end
|
end
|
||||||
if @wrestler.last_match.bracket_position == "Semis"
|
if @wrestler.last_match.bracket_position == "Semis"
|
||||||
new_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","1/2",@wrestler.next_match_position_number.ceil,@wrestler.weight_id).first
|
new_match = @matches.find { |m| m.bracket_position == "1/2" && m.bracket_position_number == @wrestler.next_match_position_number.ceil && m.weight_id == @wrestler.weight_id }
|
||||||
updateNewMatch(new_match)
|
updateNewMatch(new_match)
|
||||||
end
|
end
|
||||||
if @wrestler.last_match.bracket_position == "Conso Semis"
|
if @wrestler.last_match.bracket_position == "Conso Semis"
|
||||||
new_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","5/6",@wrestler.next_match_position_number.ceil,@wrestler.weight_id).first
|
new_match = @matches.find { |m| m.bracket_position == "5/6" && m.bracket_position_number == @wrestler.next_match_position_number.ceil && m.weight_id == @wrestler.weight_id }
|
||||||
updateNewMatch(new_match)
|
updateNewMatch(new_match)
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def updateNewMatch(match)
|
def updateNewMatch(match)
|
||||||
|
return unless match
|
||||||
if @wrestler.next_match_position_number == @wrestler.next_match_position_number.ceil
|
if @wrestler.next_match_position_number == @wrestler.next_match_position_number.ceil
|
||||||
match.w2 = @wrestler.id
|
match.w2 = @wrestler.id
|
||||||
match.save
|
|
||||||
end
|
end
|
||||||
if @wrestler.next_match_position_number != @wrestler.next_match_position_number.ceil
|
if @wrestler.next_match_position_number != @wrestler.next_match_position_number.ceil
|
||||||
match.w1 = @wrestler.id
|
match.w1 = @wrestler.id
|
||||||
match.save
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def loser_advance
|
def loser_advance
|
||||||
bout = @wrestler.last_match.bout_number
|
bout = @wrestler.last_match.bout_number
|
||||||
next_match = Match.where("(loser1_name = ? OR loser2_name = ?) AND weight_id = ?","Loser of #{bout}","Loser of #{bout}",@wrestler.weight_id)
|
next_match = @matches.find { |m| m.weight_id == @wrestler.weight_id && (m.loser1_name == "Loser of #{bout}" || m.loser2_name == "Loser of #{bout}") }
|
||||||
if next_match.size > 0
|
if next_match
|
||||||
next_match.first.replace_loser_name_with_wrestler(@wrestler,"Loser of #{bout}")
|
replace_loser_name_with_wrestler(next_match, @wrestler, "Loser of #{bout}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def replace_loser_name_with_wrestler(match, wrestler, loser_name)
|
||||||
|
match.w1 = wrestler.id if match.loser1_name == loser_name
|
||||||
|
match.w2 = wrestler.id if match.loser2_name == loser_name
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ class PoolOrder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def getPoolOrder
|
def getPoolOrder
|
||||||
# clear caching for weight for bracket page
|
|
||||||
@wrestlers.first.weight.touch
|
|
||||||
setOriginalPoints
|
setOriginalPoints
|
||||||
while checkForTies(@wrestlers) == true
|
while checkForTies(@wrestlers) == true
|
||||||
getWrestlersOrderByPoolAdvancePoints.each do |wrestler|
|
getWrestlersOrderByPoolAdvancePoints.each do |wrestler|
|
||||||
@@ -18,7 +16,6 @@ class PoolOrder
|
|||||||
getWrestlersOrderByPoolAdvancePoints.each_with_index do |wrestler, index|
|
getWrestlersOrderByPoolAdvancePoints.each_with_index do |wrestler, index|
|
||||||
placement = index + 1
|
placement = index + 1
|
||||||
wrestler.pool_placement = placement
|
wrestler.pool_placement = placement
|
||||||
wrestler.save
|
|
||||||
end
|
end
|
||||||
@wrestlers.sort_by{|w| w.poolAdvancePoints}.reverse!
|
@wrestlers.sort_by{|w| w.poolAdvancePoints}.reverse!
|
||||||
end
|
end
|
||||||
@@ -29,7 +26,6 @@ class PoolOrder
|
|||||||
|
|
||||||
def setOriginalPoints
|
def setOriginalPoints
|
||||||
@wrestlers.each do |w|
|
@wrestlers.each do |w|
|
||||||
matches = w.reload.all_matches
|
|
||||||
w.pool_placement_tiebreaker = nil
|
w.pool_placement_tiebreaker = nil
|
||||||
w.pool_placement = nil
|
w.pool_placement = nil
|
||||||
w.poolAdvancePoints = w.pool_wins.size
|
w.poolAdvancePoints = w.pool_wins.size
|
||||||
|
|||||||
55
app/services/school_services/calculate_school_score.rb
Normal file
55
app/services/school_services/calculate_school_score.rb
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
class CalculateSchoolScore
|
||||||
|
def initialize(school)
|
||||||
|
@school = school
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate
|
||||||
|
school = preload_school_context
|
||||||
|
score = calculate_score_value(school)
|
||||||
|
persist_school_score(school.id, score)
|
||||||
|
score
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_value
|
||||||
|
school = preload_school_context
|
||||||
|
calculate_score_value(school)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def preload_school_context
|
||||||
|
School
|
||||||
|
.includes(
|
||||||
|
:deductedPoints,
|
||||||
|
wrestlers: [
|
||||||
|
:deductedPoints,
|
||||||
|
{ matches_as_w1: :winner },
|
||||||
|
{ matches_as_w2: :winner },
|
||||||
|
{ weight: [:matches, { tournament: { weights: :wrestlers } }] }
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.find(@school.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_score_value(school)
|
||||||
|
total_points_scored_by_wrestlers(school) - total_points_deducted(school)
|
||||||
|
end
|
||||||
|
|
||||||
|
def total_points_scored_by_wrestlers(school)
|
||||||
|
school.wrestlers.sum { |wrestler| CalculateWrestlerTeamScore.new(wrestler).totalScore }
|
||||||
|
end
|
||||||
|
|
||||||
|
def total_points_deducted(school)
|
||||||
|
school.deductedPoints.sum(&:points)
|
||||||
|
end
|
||||||
|
|
||||||
|
def persist_school_score(school_id, score)
|
||||||
|
School.upsert_all([
|
||||||
|
{
|
||||||
|
id: school_id,
|
||||||
|
score: score,
|
||||||
|
updated_at: Time.current
|
||||||
|
}
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,33 +3,30 @@ class DoubleEliminationGenerateLoserNames
|
|||||||
@tournament = tournament
|
@tournament = tournament
|
||||||
end
|
end
|
||||||
|
|
||||||
# Entry point: assign loser placeholders and advance any byes
|
# Compatibility wrapper. Returns transformed rows and does not persist.
|
||||||
def assign_loser_names
|
def assign_loser_names(match_rows = nil)
|
||||||
|
rows = match_rows || @tournament.matches.where(tournament_id: @tournament.id).map { |m| m.attributes.symbolize_keys }
|
||||||
@tournament.weights.each do |weight|
|
@tournament.weights.each do |weight|
|
||||||
# only assign loser names if there's conso matches to be had
|
next unless weight.calculate_bracket_size > 2
|
||||||
if weight.calculate_bracket_size > 2
|
|
||||||
assign_loser_names_for_weight(weight)
|
assign_loser_names_in_memory(weight, rows)
|
||||||
advance_bye_matches_championship(weight)
|
assign_bye_outcomes_in_memory(weight, rows)
|
||||||
advance_bye_matches_consolation(weight)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
rows
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def assign_loser_names_in_memory(weight, match_rows)
|
||||||
|
|
||||||
# Assign loser names for a single weight bracket
|
|
||||||
def assign_loser_names_for_weight(weight)
|
|
||||||
bracket_size = weight.calculate_bracket_size
|
bracket_size = weight.calculate_bracket_size
|
||||||
matches = weight.matches.reload
|
return if bracket_size <= 2
|
||||||
num_placers = @tournament.number_of_placers
|
|
||||||
|
rows = match_rows.select { |row| row[:weight_id] == weight.id }
|
||||||
|
num_placers = @tournament.number_of_placers
|
||||||
|
|
||||||
# Build dynamic round definitions
|
|
||||||
champ_rounds = dynamic_championship_rounds(bracket_size)
|
champ_rounds = dynamic_championship_rounds(bracket_size)
|
||||||
conso_rounds = dynamic_consolation_rounds(bracket_size)
|
conso_rounds = dynamic_consolation_rounds(bracket_size)
|
||||||
first_round = { bracket_position: first_round_label(bracket_size) }
|
first_round = { bracket_position: first_round_label(bracket_size) }
|
||||||
champ_full = [first_round] + champ_rounds
|
champ_full = [first_round] + champ_rounds
|
||||||
|
|
||||||
# Map championship losers into consolation slots
|
|
||||||
mappings = []
|
mappings = []
|
||||||
champ_full[0...-1].each_with_index do |champ_info, i|
|
champ_full[0...-1].each_with_index do |champ_info, i|
|
||||||
map_idx = i.zero? ? 0 : (2 * i - 1)
|
map_idx = i.zero? ? 0 : (2 * i - 1)
|
||||||
@@ -37,128 +34,109 @@ class DoubleEliminationGenerateLoserNames
|
|||||||
|
|
||||||
mappings << {
|
mappings << {
|
||||||
championship_bracket_position: champ_info[:bracket_position],
|
championship_bracket_position: champ_info[:bracket_position],
|
||||||
consolation_bracket_position: conso_rounds[map_idx][:bracket_position],
|
consolation_bracket_position: conso_rounds[map_idx][:bracket_position],
|
||||||
both_wrestlers: i.zero?,
|
both_wrestlers: i.zero?,
|
||||||
champ_round_index: i
|
champ_round_index: i
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Apply loser-name mappings
|
|
||||||
mappings.each do |map|
|
mappings.each do |map|
|
||||||
champ = matches.select { |m| m.bracket_position == map[:championship_bracket_position] }
|
champ = rows.select { |r| r[:bracket_position] == map[:championship_bracket_position] }
|
||||||
.sort_by(&:bracket_position_number)
|
.sort_by { |r| r[:bracket_position_number] }
|
||||||
conso = matches.select { |m| m.bracket_position == map[:consolation_bracket_position] }
|
conso = rows.select { |r| r[:bracket_position] == map[:consolation_bracket_position] }
|
||||||
.sort_by(&:bracket_position_number)
|
.sort_by { |r| r[:bracket_position_number] }
|
||||||
|
conso.reverse! if map[:champ_round_index].odd?
|
||||||
current_champ_round_index = map[:champ_round_index]
|
|
||||||
if current_champ_round_index.odd?
|
|
||||||
conso.reverse!
|
|
||||||
end
|
|
||||||
|
|
||||||
idx = 0
|
idx = 0
|
||||||
# Determine if this mapping is for losers from the first championship round
|
is_first_feed = map[:champ_round_index].zero?
|
||||||
is_first_champ_round_feed = map[:champ_round_index].zero?
|
|
||||||
|
|
||||||
conso.each do |cm|
|
conso.each do |cm|
|
||||||
champ_match1 = champ[idx]
|
champ_match1 = champ[idx]
|
||||||
if champ_match1
|
if champ_match1
|
||||||
if is_first_champ_round_feed && ((champ_match1.w1 && champ_match1.w2.nil?) || (champ_match1.w1.nil? && champ_match1.w2))
|
if is_first_feed && single_competitor_match_row?(champ_match1)
|
||||||
cm.loser1_name = "BYE"
|
cm[:loser1_name] = "BYE"
|
||||||
else
|
else
|
||||||
cm.loser1_name = "Loser of #{champ_match1.bout_number}"
|
cm[:loser1_name] = "Loser of #{champ_match1[:bout_number]}"
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
cm.loser1_name = nil # Should not happen if bracket generation is correct
|
cm[:loser1_name] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
if map[:both_wrestlers] # This is true only if is_first_champ_round_feed
|
if map[:both_wrestlers]
|
||||||
idx += 1 # Increment for the second championship match
|
idx += 1
|
||||||
champ_match2 = champ[idx]
|
champ_match2 = champ[idx]
|
||||||
if champ_match2
|
if champ_match2
|
||||||
# BYE check is only relevant for the first championship round feed
|
if is_first_feed && single_competitor_match_row?(champ_match2)
|
||||||
if is_first_champ_round_feed && ((champ_match2.w1 && champ_match2.w2.nil?) || (champ_match2.w1.nil? && champ_match2.w2))
|
cm[:loser2_name] = "BYE"
|
||||||
cm.loser2_name = "BYE"
|
|
||||||
else
|
else
|
||||||
cm.loser2_name = "Loser of #{champ_match2.bout_number}"
|
cm[:loser2_name] = "Loser of #{champ_match2[:bout_number]}"
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
cm.loser2_name = nil # Should not happen
|
cm[:loser2_name] = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
idx += 1 # Increment for the next consolation match or next pair from championship
|
idx += 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# 5th/6th place
|
|
||||||
if bracket_size >= 5 && num_placers >= 6 && weight.wrestlers.size > 4
|
if bracket_size >= 5 && num_placers >= 6 && weight.wrestlers.size > 4
|
||||||
conso_semis = matches.select { |m| m.bracket_position == "Conso Semis" }
|
conso_semis = rows.select { |r| r[:bracket_position] == "Conso Semis" }.sort_by { |r| r[:bracket_position_number] }
|
||||||
.sort_by(&:bracket_position_number)
|
m56 = rows.find { |r| r[:bracket_position] == "5/6" }
|
||||||
if conso_semis.size >= 2
|
if conso_semis.size >= 2 && m56
|
||||||
m56 = matches.find { |m| m.bracket_position == "5/6" }
|
m56[:loser1_name] = "Loser of #{conso_semis[0][:bout_number]}"
|
||||||
m56.loser1_name = "Loser of #{conso_semis[0].bout_number}"
|
m56[:loser2_name] = "Loser of #{conso_semis[1][:bout_number]}"
|
||||||
m56.loser2_name = "Loser of #{conso_semis[1].bout_number}" if m56
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# 7th/8th place
|
|
||||||
if bracket_size >= 7 && num_placers >= 8 && weight.wrestlers.size > 6
|
if bracket_size >= 7 && num_placers >= 8 && weight.wrestlers.size > 6
|
||||||
conso_quarters = matches.select { |m| m.bracket_position == "Conso Quarter" }
|
conso_quarters = rows.select { |r| r[:bracket_position] == "Conso Quarter" }.sort_by { |r| r[:bracket_position_number] }
|
||||||
.sort_by(&:bracket_position_number)
|
m78 = rows.find { |r| r[:bracket_position] == "7/8" }
|
||||||
if conso_quarters.size >= 2
|
if conso_quarters.size >= 2 && m78
|
||||||
m78 = matches.find { |m| m.bracket_position == "7/8" }
|
m78[:loser1_name] = "Loser of #{conso_quarters[0][:bout_number]}"
|
||||||
m78.loser1_name = "Loser of #{conso_quarters[0].bout_number}"
|
m78[:loser2_name] = "Loser of #{conso_quarters[1][:bout_number]}"
|
||||||
m78.loser2_name = "Loser of #{conso_quarters[1].bout_number}" if m78
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
matches.each(&:save!)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Advance first-round byes in championship bracket
|
def assign_bye_outcomes_in_memory(weight, match_rows)
|
||||||
def advance_bye_matches_championship(weight)
|
|
||||||
matches = weight.matches.reload
|
|
||||||
first_round = matches.map(&:round).min
|
|
||||||
matches.select { |m| m.round == first_round }
|
|
||||||
.sort_by(&:bracket_position_number)
|
|
||||||
.each { |m| handle_bye(m) }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Advance first-round byes in consolation bracket
|
|
||||||
def advance_bye_matches_consolation(weight)
|
|
||||||
matches = weight.matches.reload
|
|
||||||
bracket_size = weight.calculate_bracket_size
|
bracket_size = weight.calculate_bracket_size
|
||||||
first_conso = dynamic_consolation_rounds(bracket_size).first
|
return if bracket_size <= 2
|
||||||
|
|
||||||
matches.select { |m| m.round == first_conso[:round] && m.bracket_position == first_conso[:bracket_position] }
|
rows = match_rows.select { |r| r[:weight_id] == weight.id }
|
||||||
.sort_by(&:bracket_position_number)
|
first_round = rows.map { |r| r[:round] }.compact.min
|
||||||
.each { |m| handle_bye(m) }
|
rows.select { |r| r[:round] == first_round }.each { |row| apply_bye_to_row(row) }
|
||||||
end
|
|
||||||
|
|
||||||
# Mark bye match, set finished, and advance
|
first_conso = dynamic_consolation_rounds(bracket_size).first
|
||||||
def handle_bye(match)
|
if first_conso
|
||||||
if [match.w1, match.w2].compact.size == 1
|
rows.select { |r| r[:round] == first_conso[:round] && r[:bracket_position] == first_conso[:bracket_position] }
|
||||||
match.finished = 1
|
.each { |row| apply_bye_to_row(row) }
|
||||||
match.win_type = 'BYE'
|
|
||||||
if match.w1
|
|
||||||
match.winner_id = match.w1
|
|
||||||
match.loser2_name = 'BYE'
|
|
||||||
else
|
|
||||||
match.winner_id = match.w2
|
|
||||||
match.loser1_name = 'BYE'
|
|
||||||
end
|
|
||||||
match.score = ''
|
|
||||||
match.save!
|
|
||||||
match.advance_wrestlers
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helpers for dynamic bracket labels
|
def apply_bye_to_row(row)
|
||||||
|
return unless single_competitor_match_row?(row)
|
||||||
|
|
||||||
|
row[:finished] = 1
|
||||||
|
row[:win_type] = "BYE"
|
||||||
|
if row[:w1]
|
||||||
|
row[:winner_id] = row[:w1]
|
||||||
|
row[:loser2_name] = "BYE"
|
||||||
|
else
|
||||||
|
row[:winner_id] = row[:w2]
|
||||||
|
row[:loser1_name] = "BYE"
|
||||||
|
end
|
||||||
|
row[:score] = ""
|
||||||
|
end
|
||||||
|
|
||||||
|
def single_competitor_match_row?(row)
|
||||||
|
[row[:w1], row[:w2]].compact.size == 1
|
||||||
|
end
|
||||||
|
|
||||||
def first_round_label(size)
|
def first_round_label(size)
|
||||||
case size
|
case size
|
||||||
when 2 then 'Final'
|
when 2 then "Final"
|
||||||
when 4 then 'Semis'
|
when 4 then "Semis"
|
||||||
when 8 then 'Quarter'
|
when 8 then "Quarter"
|
||||||
else "Bracket Round of #{size}"
|
else "Bracket Round of #{size}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -186,10 +164,10 @@ class DoubleEliminationGenerateLoserNames
|
|||||||
|
|
||||||
def bracket_label(participants)
|
def bracket_label(participants)
|
||||||
case participants
|
case participants
|
||||||
when 2 then '1/2'
|
when 2 then "1/2"
|
||||||
when 4 then 'Semis'
|
when 4 then "Semis"
|
||||||
when 8 then 'Quarter'
|
when 8 then "Quarter"
|
||||||
else "Bracket Round of #{participants}"
|
else "Bracket Round of #{participants}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -197,12 +175,12 @@ class DoubleEliminationGenerateLoserNames
|
|||||||
max_j_for_bracket = (2 * (Math.log2(bracket_size).to_i - 1) - 1)
|
max_j_for_bracket = (2 * (Math.log2(bracket_size).to_i - 1) - 1)
|
||||||
|
|
||||||
if participants == 2 && j == max_j_for_bracket
|
if participants == 2 && j == max_j_for_bracket
|
||||||
return '3/4'
|
"3/4"
|
||||||
elsif participants == 4
|
elsif participants == 4
|
||||||
return j.odd? ? 'Conso Quarter' : 'Conso Semis'
|
j.odd? ? "Conso Quarter" : "Conso Semis"
|
||||||
else
|
else
|
||||||
suffix = j.odd? ? ".1" : ".2"
|
suffix = j.odd? ? ".1" : ".2"
|
||||||
return "Conso Round of #{participants}#{suffix}"
|
"Conso Round of #{participants}#{suffix}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,29 +1,33 @@
|
|||||||
class DoubleEliminationMatchGeneration
|
class DoubleEliminationMatchGeneration
|
||||||
def initialize(tournament)
|
def initialize(tournament, weights: nil)
|
||||||
@tournament = tournament
|
@tournament = tournament
|
||||||
|
@weights = weights
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_matches
|
def generate_matches
|
||||||
#
|
build_match_rows
|
||||||
# PHASE 1: Generate matches (with local round definitions).
|
end
|
||||||
#
|
|
||||||
@tournament.weights.each do |weight|
|
def build_match_rows
|
||||||
generate_matches_for_weight(weight)
|
rows_by_weight_id = {}
|
||||||
|
|
||||||
|
generation_weights.each do |weight|
|
||||||
|
rows_by_weight_id[weight.id] = generate_match_rows_for_weight(weight)
|
||||||
end
|
end
|
||||||
|
|
||||||
#
|
align_rows_to_largest_bracket(rows_by_weight_id)
|
||||||
# PHASE 2: Align all rounds to match the largest bracket’s definitions.
|
rows_by_weight_id.values.flatten
|
||||||
#
|
|
||||||
align_all_rounds_to_largest_bracket
|
|
||||||
end
|
end
|
||||||
|
|
||||||
###########################################################################
|
###########################################################################
|
||||||
# PHASE 1: Generate all matches for each bracket, using a single definition.
|
# PHASE 1: Generate all matches for each bracket, using a single definition.
|
||||||
###########################################################################
|
###########################################################################
|
||||||
def generate_matches_for_weight(weight)
|
def generate_match_rows_for_weight(weight)
|
||||||
bracket_size = weight.calculate_bracket_size
|
bracket_size = weight.calculate_bracket_size
|
||||||
bracket_info = define_bracket_matches(bracket_size)
|
bracket_info = define_bracket_matches(bracket_size)
|
||||||
return unless bracket_info
|
return [] unless bracket_info
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
|
||||||
# 1) Round one matchups
|
# 1) Round one matchups
|
||||||
bracket_info[:round_one_matchups].each_with_index do |matchup, idx|
|
bracket_info[:round_one_matchups].each_with_index do |matchup, idx|
|
||||||
@@ -32,7 +36,7 @@ class DoubleEliminationMatchGeneration
|
|||||||
bracket_pos_number = idx + 1
|
bracket_pos_number = idx + 1
|
||||||
round_number = matchup[:round]
|
round_number = matchup[:round]
|
||||||
|
|
||||||
create_matchup_from_seed(
|
rows << create_matchup_from_seed(
|
||||||
seed1,
|
seed1,
|
||||||
seed2,
|
seed2,
|
||||||
bracket_position,
|
bracket_position,
|
||||||
@@ -49,7 +53,7 @@ class DoubleEliminationMatchGeneration
|
|||||||
round_number = round_info[:round]
|
round_number = round_info[:round]
|
||||||
|
|
||||||
matches_this_round.times do |i|
|
matches_this_round.times do |i|
|
||||||
create_matchup(
|
rows << create_matchup(
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
bracket_position,
|
bracket_position,
|
||||||
@@ -67,7 +71,7 @@ class DoubleEliminationMatchGeneration
|
|||||||
round_number = round_info[:round]
|
round_number = round_info[:round]
|
||||||
|
|
||||||
matches_this_round.times do |i|
|
matches_this_round.times do |i|
|
||||||
create_matchup(
|
rows << create_matchup(
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
bracket_position,
|
bracket_position,
|
||||||
@@ -79,12 +83,14 @@ class DoubleEliminationMatchGeneration
|
|||||||
|
|
||||||
# 5/6, 7/8 placing logic
|
# 5/6, 7/8 placing logic
|
||||||
if weight.wrestlers.size >= 5 && @tournament.number_of_placers >= 6 && matches_this_round == 1
|
if weight.wrestlers.size >= 5 && @tournament.number_of_placers >= 6 && matches_this_round == 1
|
||||||
create_matchup(nil, nil, "5/6", 1, round_number, weight)
|
rows << create_matchup(nil, nil, "5/6", 1, round_number, weight)
|
||||||
end
|
end
|
||||||
if weight.wrestlers.size >= 7 && @tournament.number_of_placers >= 8 && matches_this_round == 1
|
if weight.wrestlers.size >= 7 && @tournament.number_of_placers >= 8 && matches_this_round == 1
|
||||||
create_matchup(nil, nil, "7/8", 1, round_number, weight)
|
rows << create_matchup(nil, nil, "7/8", 1, round_number, weight)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rows
|
||||||
end
|
end
|
||||||
|
|
||||||
# Single bracket definition dynamically generated for any power-of-two bracket size.
|
# Single bracket definition dynamically generated for any power-of-two bracket size.
|
||||||
@@ -173,18 +179,18 @@ class DoubleEliminationMatchGeneration
|
|||||||
###########################################################################
|
###########################################################################
|
||||||
# PHASE 2: Overwrite rounds in all smaller brackets to match the largest one.
|
# PHASE 2: Overwrite rounds in all smaller brackets to match the largest one.
|
||||||
###########################################################################
|
###########################################################################
|
||||||
def align_all_rounds_to_largest_bracket
|
def align_rows_to_largest_bracket(rows_by_weight_id)
|
||||||
largest_weight = @tournament.weights.max_by { |w| w.calculate_bracket_size }
|
largest_weight = generation_weights.max_by { |w| w.calculate_bracket_size }
|
||||||
return unless largest_weight
|
return unless largest_weight
|
||||||
|
|
||||||
position_to_round = {}
|
position_to_round = {}
|
||||||
largest_weight.tournament.matches.where(weight_id: largest_weight.id).each do |m|
|
rows_by_weight_id.fetch(largest_weight.id, []).each do |row|
|
||||||
position_to_round[m.bracket_position] ||= m.round
|
position_to_round[row[:bracket_position]] ||= row[:round]
|
||||||
end
|
end
|
||||||
|
|
||||||
@tournament.matches.find_each do |match|
|
rows_by_weight_id.each_value do |rows|
|
||||||
if position_to_round.key?(match.bracket_position)
|
rows.each do |row|
|
||||||
match.update(round: position_to_round[match.bracket_position])
|
row[:round] = position_to_round[row[:bracket_position]] if position_to_round.key?(row[:bracket_position])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -192,8 +198,12 @@ class DoubleEliminationMatchGeneration
|
|||||||
###########################################################################
|
###########################################################################
|
||||||
# Helper methods
|
# Helper methods
|
||||||
###########################################################################
|
###########################################################################
|
||||||
|
def generation_weights
|
||||||
|
@weights || @tournament.weights.to_a
|
||||||
|
end
|
||||||
|
|
||||||
def wrestler_with_seed(seed, weight)
|
def wrestler_with_seed(seed, weight)
|
||||||
Wrestler.where("weight_id = ? AND bracket_line = ?", weight.id, seed).first&.id
|
weight.wrestlers.find { |w| w.bracket_line == seed }&.id
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_matchup_from_seed(w1_seed, w2_seed, bracket_position, bracket_position_number, round, weight)
|
def create_matchup_from_seed(w1_seed, w2_seed, bracket_position, bracket_position_number, round, weight)
|
||||||
@@ -208,14 +218,15 @@ class DoubleEliminationMatchGeneration
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_matchup(w1, w2, bracket_position, bracket_position_number, round, weight)
|
def create_matchup(w1, w2, bracket_position, bracket_position_number, round, weight)
|
||||||
weight.tournament.matches.create!(
|
{
|
||||||
w1: w1,
|
w1: w1,
|
||||||
w2: w2,
|
w2: w2,
|
||||||
|
tournament_id: weight.tournament_id,
|
||||||
weight_id: weight.id,
|
weight_id: weight.id,
|
||||||
round: round,
|
round: round,
|
||||||
bracket_position: bracket_position,
|
bracket_position: bracket_position,
|
||||||
bracket_position_number: bracket_position_number
|
bracket_position_number: bracket_position_number
|
||||||
)
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Calculates the sequence of seeds for the first round of a power-of-two bracket.
|
# Calculates the sequence of seeds for the first round of a power-of-two bracket.
|
||||||
|
|||||||
@@ -10,62 +10,183 @@ class GenerateTournamentMatches
|
|||||||
|
|
||||||
def generate_raw
|
def generate_raw
|
||||||
standardStartingActions
|
standardStartingActions
|
||||||
PoolToBracketMatchGeneration.new(@tournament).generatePoolToBracketMatches if @tournament.tournament_type == "Pool to bracket"
|
generation_context = preload_generation_context
|
||||||
ModifiedSixteenManMatchGeneration.new(@tournament).generate_matches if @tournament.tournament_type.include? "Modified 16 Man Double Elimination"
|
seed_wrestlers_in_memory(generation_context)
|
||||||
DoubleEliminationMatchGeneration.new(@tournament).generate_matches if @tournament.tournament_type.include? "Regular Double Elimination"
|
match_rows = build_match_rows(generation_context)
|
||||||
|
post_process_match_rows_in_memory(generation_context, match_rows)
|
||||||
|
persist_generation_rows(generation_context, match_rows)
|
||||||
postMatchCreationActions
|
postMatchCreationActions
|
||||||
PoolToBracketMatchGeneration.new(@tournament).assignLoserNames if @tournament.tournament_type == "Pool to bracket"
|
advance_bye_matches_after_insert
|
||||||
ModifiedSixteenManGenerateLoserNames.new(@tournament).assign_loser_names if @tournament.tournament_type.include? "Modified 16 Man Double Elimination"
|
|
||||||
DoubleEliminationGenerateLoserNames.new(@tournament).assign_loser_names if @tournament.tournament_type.include? "Regular Double Elimination"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def standardStartingActions
|
def standardStartingActions
|
||||||
@tournament.curently_generating_matches = 1
|
@tournament.curently_generating_matches = 1
|
||||||
@tournament.save
|
@tournament.save
|
||||||
WipeTournamentMatches.new(@tournament).setUpMatchGeneration
|
WipeTournamentMatches.new(@tournament).setUpMatchGeneration
|
||||||
TournamentSeeding.new(@tournament).set_seeds
|
end
|
||||||
|
|
||||||
|
def preload_generation_context
|
||||||
|
weights = @tournament.weights.includes(:wrestlers).order(:max).to_a
|
||||||
|
wrestlers = weights.flat_map(&:wrestlers)
|
||||||
|
{
|
||||||
|
weights: weights,
|
||||||
|
wrestlers: wrestlers,
|
||||||
|
wrestlers_by_weight_id: wrestlers.group_by(&:weight_id)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def seed_wrestlers_in_memory(generation_context)
|
||||||
|
TournamentSeeding.new(@tournament).set_seeds(weights: generation_context[:weights], persist: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_match_rows(generation_context)
|
||||||
|
return PoolToBracketMatchGeneration.new(
|
||||||
|
@tournament,
|
||||||
|
weights: generation_context[:weights],
|
||||||
|
wrestlers_by_weight_id: generation_context[:wrestlers_by_weight_id]
|
||||||
|
).generatePoolToBracketMatches if @tournament.tournament_type == "Pool to bracket"
|
||||||
|
|
||||||
|
return ModifiedSixteenManMatchGeneration.new(@tournament, weights: generation_context[:weights]).generate_matches if @tournament.tournament_type.include? "Modified 16 Man Double Elimination"
|
||||||
|
|
||||||
|
return DoubleEliminationMatchGeneration.new(@tournament, weights: generation_context[:weights]).generate_matches if @tournament.tournament_type.include? "Regular Double Elimination"
|
||||||
|
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
def persist_generation_rows(generation_context, match_rows)
|
||||||
|
persist_wrestlers(generation_context[:wrestlers])
|
||||||
|
persist_matches(match_rows)
|
||||||
|
end
|
||||||
|
|
||||||
|
def post_process_match_rows_in_memory(generation_context, match_rows)
|
||||||
|
move_finals_rows_to_last_round(match_rows) unless @tournament.tournament_type.include?("Regular Double Elimination")
|
||||||
|
assign_bouts_in_memory(match_rows, generation_context[:weights])
|
||||||
|
assign_loser_names_in_memory(generation_context, match_rows)
|
||||||
|
assign_bye_outcomes_in_memory(generation_context, match_rows)
|
||||||
|
end
|
||||||
|
|
||||||
|
def persist_wrestlers(wrestlers)
|
||||||
|
return if wrestlers.blank?
|
||||||
|
|
||||||
|
timestamp = Time.current
|
||||||
|
rows = wrestlers.map do |w|
|
||||||
|
{
|
||||||
|
id: w.id,
|
||||||
|
bracket_line: w.bracket_line,
|
||||||
|
pool: w.pool,
|
||||||
|
updated_at: timestamp
|
||||||
|
}
|
||||||
|
end
|
||||||
|
Wrestler.upsert_all(rows)
|
||||||
|
end
|
||||||
|
|
||||||
|
def persist_matches(match_rows)
|
||||||
|
return if match_rows.blank?
|
||||||
|
|
||||||
|
timestamp = Time.current
|
||||||
|
rows_with_timestamps = match_rows.map do |row|
|
||||||
|
row.to_h.symbolize_keys.merge(created_at: timestamp, updated_at: timestamp)
|
||||||
|
end
|
||||||
|
|
||||||
|
all_keys = rows_with_timestamps.flat_map(&:keys).uniq
|
||||||
|
normalized_rows = rows_with_timestamps.map do |row|
|
||||||
|
all_keys.each_with_object({}) { |key, normalized| normalized[key] = row[key] }
|
||||||
|
end
|
||||||
|
|
||||||
|
Match.insert_all!(normalized_rows)
|
||||||
end
|
end
|
||||||
|
|
||||||
def postMatchCreationActions
|
def postMatchCreationActions
|
||||||
moveFinalsMatchesToLastRound if ! @tournament.tournament_type.include? "Regular Double Elimination"
|
|
||||||
assignBouts
|
|
||||||
@tournament.reset_and_fill_bout_board
|
@tournament.reset_and_fill_bout_board
|
||||||
@tournament.curently_generating_matches = nil
|
@tournament.curently_generating_matches = nil
|
||||||
@tournament.save!
|
@tournament.save!
|
||||||
|
Tournament.broadcast_up_matches_board(@tournament.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def move_finals_rows_to_last_round(match_rows)
|
||||||
|
finals_round = match_rows.map { |row| row[:round] }.compact.max
|
||||||
|
return unless finals_round
|
||||||
|
|
||||||
|
match_rows.each do |row|
|
||||||
|
row[:round] = finals_round if ["1/2", "3/4", "5/6", "7/8"].include?(row[:bracket_position])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_bouts_in_memory(match_rows, weights)
|
||||||
|
bout_counts = Hash.new(0)
|
||||||
|
weight_max_by_id = weights.each_with_object({}) { |w, memo| memo[w.id] = w.max }
|
||||||
|
|
||||||
|
match_rows
|
||||||
|
.sort_by { |row| [row[:round].to_i, weight_max_by_id[row[:weight_id]].to_f, row[:weight_id].to_i, row[:bracket_position_number].to_i] }
|
||||||
|
.each do |row|
|
||||||
|
round = row[:round].to_i
|
||||||
|
row[:bout_number] = round * 1000 + bout_counts[round]
|
||||||
|
bout_counts[round] += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_loser_names_in_memory(generation_context, match_rows)
|
||||||
|
if @tournament.tournament_type == "Pool to bracket"
|
||||||
|
service = PoolToBracketGenerateLoserNames.new(@tournament)
|
||||||
|
generation_context[:weights].each { |weight| service.assign_loser_names_in_memory(weight, match_rows) }
|
||||||
|
elsif @tournament.tournament_type.include?("Modified 16 Man Double Elimination")
|
||||||
|
service = ModifiedSixteenManGenerateLoserNames.new(@tournament)
|
||||||
|
generation_context[:weights].each { |weight| service.assign_loser_names_in_memory(weight, match_rows) }
|
||||||
|
elsif @tournament.tournament_type.include?("Regular Double Elimination")
|
||||||
|
service = DoubleEliminationGenerateLoserNames.new(@tournament)
|
||||||
|
generation_context[:weights].each { |weight| service.assign_loser_names_in_memory(weight, match_rows) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_bye_outcomes_in_memory(generation_context, match_rows)
|
||||||
|
if @tournament.tournament_type.include?("Modified 16 Man Double Elimination")
|
||||||
|
service = ModifiedSixteenManGenerateLoserNames.new(@tournament)
|
||||||
|
generation_context[:weights].each { |weight| service.assign_bye_outcomes_in_memory(weight, match_rows) }
|
||||||
|
elsif @tournament.tournament_type.include?("Regular Double Elimination")
|
||||||
|
service = DoubleEliminationGenerateLoserNames.new(@tournament)
|
||||||
|
generation_context[:weights].each { |weight| service.assign_bye_outcomes_in_memory(weight, match_rows) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def advance_bye_matches_after_insert
|
||||||
|
Match.where(tournament_id: @tournament.id, finished: 1, win_type: "BYE")
|
||||||
|
.where.not(winner_id: nil)
|
||||||
|
.find_each(&:advance_wrestlers)
|
||||||
end
|
end
|
||||||
|
|
||||||
def assignBouts
|
def assignBouts
|
||||||
bout_counts = Hash.new(0)
|
bout_counts = Hash.new(0)
|
||||||
@tournament.matches.reload
|
timestamp = Time.current
|
||||||
@tournament.matches.sort_by{|m| [m.round, m.weight_max]}.each do |m|
|
ordered_matches = Match.joins(:weight)
|
||||||
m.bout_number = m.round * 1000 + bout_counts[m.round]
|
.where(tournament_id: @tournament.id)
|
||||||
bout_counts[m.round] += 1
|
.order("matches.round ASC, weights.max ASC, matches.id ASC")
|
||||||
m.save!
|
.pluck("matches.id", "matches.round")
|
||||||
|
|
||||||
|
updates = []
|
||||||
|
ordered_matches.each do |match_id, round|
|
||||||
|
updates << {
|
||||||
|
id: match_id,
|
||||||
|
bout_number: round * 1000 + bout_counts[round],
|
||||||
|
updated_at: timestamp
|
||||||
|
}
|
||||||
|
bout_counts[round] += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Match.upsert_all(updates) if updates.any?
|
||||||
end
|
end
|
||||||
|
|
||||||
def moveFinalsMatchesToLastRound
|
def moveFinalsMatchesToLastRound
|
||||||
finalsRound = @tournament.reload.total_rounds
|
finalsRound = @tournament.reload.total_rounds
|
||||||
finalsMatches = @tournament.matches.reload.select{|m| m.bracket_position == "1/2" || m.bracket_position == "3/4" || m.bracket_position == "5/6" || m.bracket_position == "7/8"}
|
@tournament.matches
|
||||||
finalsMatches. each do |m|
|
.where(bracket_position: ["1/2", "3/4", "5/6", "7/8"])
|
||||||
m.round = finalsRound
|
.update_all(round: finalsRound, updated_at: Time.current)
|
||||||
m.save
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def unAssignMats
|
def unAssignMats
|
||||||
matches = @tournament.matches.reload
|
@tournament.matches.update_all(mat_id: nil, updated_at: Time.current)
|
||||||
matches.each do |m|
|
|
||||||
m.mat_id = nil
|
|
||||||
m.save!
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def unAssignBouts
|
def unAssignBouts
|
||||||
bout_counts = Hash.new(0)
|
@tournament.matches.update_all(bout_number: nil, updated_at: Time.current)
|
||||||
@tournament.matches.each do |m|
|
|
||||||
m.bout_number = nil
|
|
||||||
m.save!
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,95 +1,91 @@
|
|||||||
class ModifiedSixteenManGenerateLoserNames
|
class ModifiedSixteenManGenerateLoserNames
|
||||||
def initialize( tournament )
|
def initialize(tournament)
|
||||||
@tournament = tournament
|
@tournament = tournament
|
||||||
end
|
|
||||||
|
|
||||||
def assign_loser_names
|
|
||||||
matches_by_weight = nil
|
|
||||||
@tournament.weights.each do |w|
|
|
||||||
matches_by_weight = @tournament.matches.where(weight_id: w.id)
|
|
||||||
conso_round_2(matches_by_weight)
|
|
||||||
conso_round_3(matches_by_weight)
|
|
||||||
third_fourth(matches_by_weight)
|
|
||||||
seventh_eighth(matches_by_weight)
|
|
||||||
save_matches(matches_by_weight)
|
|
||||||
matches_by_weight = @tournament.matches.where(weight_id: w.id).reload
|
|
||||||
advance_bye_matches_championship(matches_by_weight)
|
|
||||||
save_matches(matches_by_weight)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def conso_round_2(matches)
|
|
||||||
matches.select{|m| m.bracket_position == "Conso Round of 8"}.sort_by{|m| m.bracket_position_number}.each do |match|
|
|
||||||
if match.bracket_position_number == 1
|
|
||||||
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 1 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
|
|
||||||
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 2 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
|
|
||||||
elsif match.bracket_position_number == 2
|
|
||||||
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 3 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
|
|
||||||
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 4 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
|
|
||||||
elsif match.bracket_position_number == 3
|
|
||||||
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 5 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
|
|
||||||
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 6 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
|
|
||||||
elsif match.bracket_position_number == 4
|
|
||||||
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 7 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
|
|
||||||
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 8 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def conso_round_3(matches)
|
|
||||||
matches.select{|m| m.bracket_position == "Conso Quarter"}.sort_by{|m| m.bracket_position_number}.each do |match|
|
|
||||||
if match.bracket_position_number == 1
|
|
||||||
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 4 and m.bracket_position == "Quarter"}.first.bout_number}"
|
|
||||||
elsif match.bracket_position_number == 2
|
|
||||||
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 3 and m.bracket_position == "Quarter"}.first.bout_number}"
|
|
||||||
elsif match.bracket_position_number == 3
|
|
||||||
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 2 and m.bracket_position == "Quarter"}.first.bout_number}"
|
|
||||||
elsif match.bracket_position_number == 4
|
|
||||||
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 1 and m.bracket_position == "Quarter"}.first.bout_number}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def third_fourth(matches)
|
|
||||||
matches.select{|m| m.bracket_position == "3/4"}.sort_by{|m| m.bracket_position_number}.each do |match|
|
|
||||||
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position == "Semis"}.first.bout_number}"
|
|
||||||
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position == "Semis"}.second.bout_number}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def seventh_eighth(matches)
|
|
||||||
matches.select{|m| m.bracket_position == "7/8"}.sort_by{|m| m.bracket_position_number}.each do |match|
|
|
||||||
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position == "Conso Semis"}.first.bout_number}"
|
|
||||||
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position == "Conso Semis"}.second.bout_number}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def advance_bye_matches_championship(matches)
|
|
||||||
matches.select{|m| m.bracket_position == "Bracket Round of 16"}.sort_by{|m| m.bracket_position_number}.each do |match|
|
|
||||||
if match.w1 == nil or match.w2 == nil
|
|
||||||
match.finished = 1
|
|
||||||
match.win_type = "BYE"
|
|
||||||
if match.w1 != nil
|
|
||||||
match.winner_id = match.w1
|
|
||||||
match.loser2_name = "BYE"
|
|
||||||
match.score = ""
|
|
||||||
match.save
|
|
||||||
match.advance_wrestlers
|
|
||||||
elsif match.w2 != nil
|
|
||||||
match.winner_id = match.w2
|
|
||||||
match.loser1_name = "BYE"
|
|
||||||
match.score = ""
|
|
||||||
match.save
|
|
||||||
match.advance_wrestlers
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def save_matches(matches)
|
|
||||||
matches.each do |m|
|
|
||||||
m.save!
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Compatibility wrapper. Returns transformed rows and does not persist.
|
||||||
|
def assign_loser_names(match_rows = nil)
|
||||||
|
rows = match_rows || @tournament.matches.where(tournament_id: @tournament.id).map { |m| m.attributes.symbolize_keys }
|
||||||
|
@tournament.weights.each do |weight|
|
||||||
|
assign_loser_names_in_memory(weight, rows)
|
||||||
|
assign_bye_outcomes_in_memory(weight, rows)
|
||||||
|
end
|
||||||
|
rows
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_loser_names_in_memory(weight, match_rows)
|
||||||
|
rows = match_rows.select { |row| row[:weight_id] == weight.id }
|
||||||
|
round_16 = rows.select { |r| r[:bracket_position] == "Bracket Round of 16" }
|
||||||
|
conso_8 = rows.select { |r| r[:bracket_position] == "Conso Round of 8" }.sort_by { |r| r[:bracket_position_number] }
|
||||||
|
|
||||||
|
conso_8.each do |row|
|
||||||
|
if row[:bracket_position_number] == 1
|
||||||
|
m1 = round_16.find { |m| m[:bracket_position_number] == 1 }
|
||||||
|
m2 = round_16.find { |m| m[:bracket_position_number] == 2 }
|
||||||
|
row[:loser1_name] = "Loser of #{m1[:bout_number]}" if m1
|
||||||
|
row[:loser2_name] = "Loser of #{m2[:bout_number]}" if m2
|
||||||
|
elsif row[:bracket_position_number] == 2
|
||||||
|
m3 = round_16.find { |m| m[:bracket_position_number] == 3 }
|
||||||
|
m4 = round_16.find { |m| m[:bracket_position_number] == 4 }
|
||||||
|
row[:loser1_name] = "Loser of #{m3[:bout_number]}" if m3
|
||||||
|
row[:loser2_name] = "Loser of #{m4[:bout_number]}" if m4
|
||||||
|
elsif row[:bracket_position_number] == 3
|
||||||
|
m5 = round_16.find { |m| m[:bracket_position_number] == 5 }
|
||||||
|
m6 = round_16.find { |m| m[:bracket_position_number] == 6 }
|
||||||
|
row[:loser1_name] = "Loser of #{m5[:bout_number]}" if m5
|
||||||
|
row[:loser2_name] = "Loser of #{m6[:bout_number]}" if m6
|
||||||
|
elsif row[:bracket_position_number] == 4
|
||||||
|
m7 = round_16.find { |m| m[:bracket_position_number] == 7 }
|
||||||
|
m8 = round_16.find { |m| m[:bracket_position_number] == 8 }
|
||||||
|
row[:loser1_name] = "Loser of #{m7[:bout_number]}" if m7
|
||||||
|
row[:loser2_name] = "Loser of #{m8[:bout_number]}" if m8
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
quarters = rows.select { |r| r[:bracket_position] == "Quarter" }
|
||||||
|
conso_quarters = rows.select { |r| r[:bracket_position] == "Conso Quarter" }.sort_by { |r| r[:bracket_position_number] }
|
||||||
|
conso_quarters.each do |row|
|
||||||
|
source = case row[:bracket_position_number]
|
||||||
|
when 1 then quarters.find { |q| q[:bracket_position_number] == 4 }
|
||||||
|
when 2 then quarters.find { |q| q[:bracket_position_number] == 3 }
|
||||||
|
when 3 then quarters.find { |q| q[:bracket_position_number] == 2 }
|
||||||
|
when 4 then quarters.find { |q| q[:bracket_position_number] == 1 }
|
||||||
|
end
|
||||||
|
row[:loser1_name] = "Loser of #{source[:bout_number]}" if source
|
||||||
|
end
|
||||||
|
|
||||||
|
semis = rows.select { |r| r[:bracket_position] == "Semis" }
|
||||||
|
third_fourth = rows.find { |r| r[:bracket_position] == "3/4" }
|
||||||
|
if third_fourth
|
||||||
|
third_fourth[:loser1_name] = "Loser of #{semis.first[:bout_number]}" if semis.first
|
||||||
|
third_fourth[:loser2_name] = "Loser of #{semis.second[:bout_number]}" if semis.second
|
||||||
|
end
|
||||||
|
|
||||||
|
conso_semis = rows.select { |r| r[:bracket_position] == "Conso Semis" }
|
||||||
|
seventh_eighth = rows.find { |r| r[:bracket_position] == "7/8" }
|
||||||
|
if seventh_eighth
|
||||||
|
seventh_eighth[:loser1_name] = "Loser of #{conso_semis.first[:bout_number]}" if conso_semis.first
|
||||||
|
seventh_eighth[:loser2_name] = "Loser of #{conso_semis.second[:bout_number]}" if conso_semis.second
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_bye_outcomes_in_memory(weight, match_rows)
|
||||||
|
rows = match_rows.select { |r| r[:weight_id] == weight.id && r[:bracket_position] == "Bracket Round of 16" }
|
||||||
|
rows.each { |row| apply_bye_to_row(row) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def apply_bye_to_row(row)
|
||||||
|
return unless [row[:w1], row[:w2]].compact.size == 1
|
||||||
|
|
||||||
|
row[:finished] = 1
|
||||||
|
row[:win_type] = "BYE"
|
||||||
|
if row[:w1]
|
||||||
|
row[:winner_id] = row[:w1]
|
||||||
|
row[:loser2_name] = "BYE"
|
||||||
|
else
|
||||||
|
row[:winner_id] = row[:w2]
|
||||||
|
row[:loser1_name] = "BYE"
|
||||||
|
end
|
||||||
|
row[:score] = ""
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -1,70 +1,75 @@
|
|||||||
class ModifiedSixteenManMatchGeneration
|
class ModifiedSixteenManMatchGeneration
|
||||||
def initialize( tournament )
|
def initialize( tournament, weights: nil )
|
||||||
@tournament = tournament
|
@tournament = tournament
|
||||||
@number_of_placers = @tournament.number_of_placers
|
@number_of_placers = @tournament.number_of_placers
|
||||||
|
@weights = weights
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_matches
|
def generate_matches
|
||||||
@tournament.weights.each do |weight|
|
rows = []
|
||||||
generate_matches_for_weight(weight)
|
generation_weights.each do |weight|
|
||||||
|
rows.concat(generate_match_rows_for_weight(weight))
|
||||||
end
|
end
|
||||||
|
rows
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_matches_for_weight(weight)
|
def generate_match_rows_for_weight(weight)
|
||||||
round_one(weight)
|
rows = []
|
||||||
round_two(weight)
|
round_one(weight, rows)
|
||||||
round_three(weight)
|
round_two(weight, rows)
|
||||||
round_four(weight)
|
round_three(weight, rows)
|
||||||
round_five(weight)
|
round_four(weight, rows)
|
||||||
|
round_five(weight, rows)
|
||||||
|
rows
|
||||||
end
|
end
|
||||||
|
|
||||||
def round_one(weight)
|
def round_one(weight, rows)
|
||||||
create_matchup_from_seed(1,16, "Bracket Round of 16", 1, 1,weight)
|
rows << create_matchup_from_seed(1,16, "Bracket Round of 16", 1, 1,weight)
|
||||||
create_matchup_from_seed(8,9, "Bracket Round of 16", 2, 1,weight)
|
rows << create_matchup_from_seed(8,9, "Bracket Round of 16", 2, 1,weight)
|
||||||
create_matchup_from_seed(5,12, "Bracket Round of 16", 3, 1,weight)
|
rows << create_matchup_from_seed(5,12, "Bracket Round of 16", 3, 1,weight)
|
||||||
create_matchup_from_seed(4,14, "Bracket Round of 16", 4, 1,weight)
|
rows << create_matchup_from_seed(4,14, "Bracket Round of 16", 4, 1,weight)
|
||||||
create_matchup_from_seed(3,13, "Bracket Round of 16", 5, 1,weight)
|
rows << create_matchup_from_seed(3,13, "Bracket Round of 16", 5, 1,weight)
|
||||||
create_matchup_from_seed(6,11, "Bracket Round of 16", 6, 1,weight)
|
rows << create_matchup_from_seed(6,11, "Bracket Round of 16", 6, 1,weight)
|
||||||
create_matchup_from_seed(7,10, "Bracket Round of 16", 7, 1,weight)
|
rows << create_matchup_from_seed(7,10, "Bracket Round of 16", 7, 1,weight)
|
||||||
create_matchup_from_seed(2,15, "Bracket Round of 16", 8, 1,weight)
|
rows << create_matchup_from_seed(2,15, "Bracket Round of 16", 8, 1,weight)
|
||||||
end
|
end
|
||||||
|
|
||||||
def round_two(weight)
|
def round_two(weight, rows)
|
||||||
create_matchup(nil,nil,"Quarter",1,2,weight)
|
rows << create_matchup(nil,nil,"Quarter",1,2,weight)
|
||||||
create_matchup(nil,nil,"Quarter",2,2,weight)
|
rows << create_matchup(nil,nil,"Quarter",2,2,weight)
|
||||||
create_matchup(nil,nil,"Quarter",3,2,weight)
|
rows << create_matchup(nil,nil,"Quarter",3,2,weight)
|
||||||
create_matchup(nil,nil,"Quarter",4,2,weight)
|
rows << create_matchup(nil,nil,"Quarter",4,2,weight)
|
||||||
create_matchup(nil,nil,"Conso Round of 8",1,2,weight)
|
rows << create_matchup(nil,nil,"Conso Round of 8",1,2,weight)
|
||||||
create_matchup(nil,nil,"Conso Round of 8",2,2,weight)
|
rows << create_matchup(nil,nil,"Conso Round of 8",2,2,weight)
|
||||||
create_matchup(nil,nil,"Conso Round of 8",3,2,weight)
|
rows << create_matchup(nil,nil,"Conso Round of 8",3,2,weight)
|
||||||
create_matchup(nil,nil,"Conso Round of 8",4,2,weight)
|
rows << create_matchup(nil,nil,"Conso Round of 8",4,2,weight)
|
||||||
end
|
end
|
||||||
|
|
||||||
def round_three(weight)
|
def round_three(weight, rows)
|
||||||
create_matchup(nil,nil,"Semis",1,3,weight)
|
rows << create_matchup(nil,nil,"Semis",1,3,weight)
|
||||||
create_matchup(nil,nil,"Semis",2,3,weight)
|
rows << create_matchup(nil,nil,"Semis",2,3,weight)
|
||||||
create_matchup(nil,nil,"Conso Quarter",1,3,weight)
|
rows << create_matchup(nil,nil,"Conso Quarter",1,3,weight)
|
||||||
create_matchup(nil,nil,"Conso Quarter",2,3,weight)
|
rows << create_matchup(nil,nil,"Conso Quarter",2,3,weight)
|
||||||
create_matchup(nil,nil,"Conso Quarter",3,3,weight)
|
rows << create_matchup(nil,nil,"Conso Quarter",3,3,weight)
|
||||||
create_matchup(nil,nil,"Conso Quarter",4,3,weight)
|
rows << create_matchup(nil,nil,"Conso Quarter",4,3,weight)
|
||||||
end
|
end
|
||||||
|
|
||||||
def round_four(weight)
|
def round_four(weight, rows)
|
||||||
create_matchup(nil,nil,"Conso Semis",1,4,weight)
|
rows << create_matchup(nil,nil,"Conso Semis",1,4,weight)
|
||||||
create_matchup(nil,nil,"Conso Semis",2,4,weight)
|
rows << create_matchup(nil,nil,"Conso Semis",2,4,weight)
|
||||||
end
|
end
|
||||||
|
|
||||||
def round_five(weight)
|
def round_five(weight, rows)
|
||||||
create_matchup(nil,nil,"1/2",1,5,weight)
|
rows << create_matchup(nil,nil,"1/2",1,5,weight)
|
||||||
create_matchup(nil,nil,"3/4",1,5,weight)
|
rows << create_matchup(nil,nil,"3/4",1,5,weight)
|
||||||
create_matchup(nil,nil,"5/6",1,5,weight)
|
rows << create_matchup(nil,nil,"5/6",1,5,weight)
|
||||||
if @number_of_placers >= 8
|
if @number_of_placers >= 8
|
||||||
create_matchup(nil,nil,"7/8",1,5,weight)
|
rows << create_matchup(nil,nil,"7/8",1,5,weight)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def wrestler_with_seed(seed,weight)
|
def wrestler_with_seed(seed,weight)
|
||||||
wrestler = Wrestler.where("weight_id = ? and bracket_line = ?", weight.id, seed).first
|
wrestler = weight.wrestlers.find { |w| w.bracket_line == seed }
|
||||||
if wrestler
|
if wrestler
|
||||||
return wrestler.id
|
return wrestler.id
|
||||||
else
|
else
|
||||||
@@ -79,13 +84,18 @@ class ModifiedSixteenManMatchGeneration
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_matchup(w1, w2, bracket_position, bracket_position_number,round,weight)
|
def create_matchup(w1, w2, bracket_position, bracket_position_number,round,weight)
|
||||||
@tournament.matches.create(
|
{
|
||||||
w1: w1,
|
w1: w1,
|
||||||
w2: w2,
|
w2: w2,
|
||||||
|
tournament_id: @tournament.id,
|
||||||
weight_id: weight.id,
|
weight_id: weight.id,
|
||||||
round: round,
|
round: round,
|
||||||
bracket_position: bracket_position,
|
bracket_position: bracket_position,
|
||||||
bracket_position_number: bracket_position_number
|
bracket_position_number: bracket_position_number
|
||||||
)
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def generation_weights
|
||||||
|
@weights || @tournament.weights.to_a
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -12,18 +12,19 @@ class PoolBracketGeneration
|
|||||||
end
|
end
|
||||||
|
|
||||||
def generateBracketMatches()
|
def generateBracketMatches()
|
||||||
|
@rows = []
|
||||||
if @pool_bracket_type == "twoPoolsToSemi"
|
if @pool_bracket_type == "twoPoolsToSemi"
|
||||||
return twoPoolsToSemi()
|
twoPoolsToSemi()
|
||||||
elsif @pool_bracket_type == "twoPoolsToFinal"
|
elsif @pool_bracket_type == "twoPoolsToFinal"
|
||||||
return twoPoolsToFinal()
|
twoPoolsToFinal()
|
||||||
elsif @pool_bracket_type == "fourPoolsToQuarter"
|
elsif @pool_bracket_type == "fourPoolsToQuarter"
|
||||||
return fourPoolsToQuarter()
|
fourPoolsToQuarter()
|
||||||
elsif @pool_bracket_type == "fourPoolsToSemi"
|
elsif @pool_bracket_type == "fourPoolsToSemi"
|
||||||
return fourPoolsToSemi()
|
fourPoolsToSemi()
|
||||||
elsif @pool_bracket_type == "eightPoolsToQuarter"
|
elsif @pool_bracket_type == "eightPoolsToQuarter"
|
||||||
return eightPoolsToQuarter()
|
eightPoolsToQuarter()
|
||||||
end
|
end
|
||||||
return []
|
@rows
|
||||||
end
|
end
|
||||||
|
|
||||||
def twoPoolsToSemi()
|
def twoPoolsToSemi()
|
||||||
@@ -86,14 +87,15 @@ class PoolBracketGeneration
|
|||||||
end
|
end
|
||||||
|
|
||||||
def createMatchup(w1_name, w2_name, bracket_position, bracket_position_number)
|
def createMatchup(w1_name, w2_name, bracket_position, bracket_position_number)
|
||||||
@tournament.matches.create(
|
@rows << {
|
||||||
loser1_name: w1_name,
|
loser1_name: w1_name,
|
||||||
loser2_name: w2_name,
|
loser2_name: w2_name,
|
||||||
|
tournament_id: @tournament.id,
|
||||||
weight_id: @weight.id,
|
weight_id: @weight.id,
|
||||||
round: @round,
|
round: @round,
|
||||||
bracket_position: bracket_position,
|
bracket_position: bracket_position,
|
||||||
bracket_position_number: bracket_position_number
|
bracket_position_number: bracket_position_number
|
||||||
)
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,35 +1,46 @@
|
|||||||
class PoolGeneration
|
class PoolGeneration
|
||||||
def initialize(weight)
|
def initialize(weight, wrestlers: nil)
|
||||||
@weight = weight
|
@weight = weight
|
||||||
@tournament = @weight.tournament
|
@tournament = @weight.tournament
|
||||||
@pool = 1
|
@pool = 1
|
||||||
|
@wrestlers = wrestlers
|
||||||
end
|
end
|
||||||
|
|
||||||
def generatePools
|
def generatePools
|
||||||
GeneratePoolNumbers.new(@weight).savePoolNumbers
|
GeneratePoolNumbers.new(@weight).savePoolNumbers(wrestlers: wrestlers_for_weight, persist: false)
|
||||||
|
rows = []
|
||||||
pools = @weight.pools
|
pools = @weight.pools
|
||||||
while @pool <= pools
|
while @pool <= pools
|
||||||
roundRobin
|
rows.concat(roundRobin)
|
||||||
@pool += 1
|
@pool += 1
|
||||||
end
|
end
|
||||||
|
rows
|
||||||
end
|
end
|
||||||
|
|
||||||
def roundRobin
|
def roundRobin
|
||||||
wrestlers = @weight.wrestlers_in_pool(@pool)
|
rows = []
|
||||||
|
wrestlers = wrestlers_for_weight.select { |w| w.pool == @pool }
|
||||||
pool_matches = RoundRobinTournament.schedule(wrestlers).reverse
|
pool_matches = RoundRobinTournament.schedule(wrestlers).reverse
|
||||||
pool_matches.each_with_index do |b, index|
|
pool_matches.each_with_index do |b, index|
|
||||||
round = index + 1
|
round = index + 1
|
||||||
bouts = b.map
|
bouts = b.map
|
||||||
bouts.each do |bout|
|
bouts.each do |bout|
|
||||||
if bout[0] != nil and bout[1] != nil
|
if bout[0] != nil and bout[1] != nil
|
||||||
@tournament.matches.create(
|
rows << {
|
||||||
w1: bout[0].id,
|
w1: bout[0].id,
|
||||||
w2: bout[1].id,
|
w2: bout[1].id,
|
||||||
|
tournament_id: @tournament.id,
|
||||||
weight_id: @weight.id,
|
weight_id: @weight.id,
|
||||||
bracket_position: "Pool",
|
bracket_position: "Pool",
|
||||||
round: round)
|
round: round
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
rows
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def wrestlers_for_weight
|
||||||
|
@wrestlers || @weight.wrestlers.to_a
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,80 +1,97 @@
|
|||||||
class PoolToBracketGenerateLoserNames
|
class PoolToBracketGenerateLoserNames
|
||||||
def initialize( tournament )
|
def initialize(tournament)
|
||||||
@tournament = tournament
|
@tournament = tournament
|
||||||
|
end
|
||||||
|
|
||||||
|
# Compatibility wrapper. Returns transformed rows and does not persist.
|
||||||
|
def assignLoserNamesWeight(weight, match_rows = nil)
|
||||||
|
rows = match_rows || @tournament.matches.where(weight_id: weight.id).map { |m| m.attributes.symbolize_keys }
|
||||||
|
assign_loser_names_in_memory(weight, rows)
|
||||||
|
rows
|
||||||
|
end
|
||||||
|
|
||||||
|
# Compatibility wrapper. Returns transformed rows and does not persist.
|
||||||
|
def assignLoserNames
|
||||||
|
@tournament.weights.each_with_object([]) do |weight, all_rows|
|
||||||
|
all_rows.concat(assignLoserNamesWeight(weight))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_loser_names_in_memory(weight, match_rows)
|
||||||
|
rows = match_rows.select { |row| row[:weight_id] == weight.id }
|
||||||
|
if weight.pool_bracket_type == "twoPoolsToSemi"
|
||||||
|
two_pools_to_semi_loser_rows(rows)
|
||||||
|
elsif (weight.pool_bracket_type == "fourPoolsToQuarter") || (weight.pool_bracket_type == "eightPoolsToQuarter")
|
||||||
|
four_pools_to_quarter_loser_rows(rows)
|
||||||
|
elsif weight.pool_bracket_type == "fourPoolsToSemi"
|
||||||
|
four_pools_to_semi_loser_rows(rows)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def two_pools_to_semi_loser_rows(rows)
|
||||||
|
match1 = rows.find { |m| m[:loser1_name] == "Winner Pool 1" }
|
||||||
|
match2 = rows.find { |m| m[:loser1_name] == "Winner Pool 2" }
|
||||||
|
match_change = rows.find { |m| m[:bracket_position] == "3/4" }
|
||||||
|
return unless match1 && match2 && match_change
|
||||||
|
|
||||||
|
match_change[:loser1_name] = "Loser of #{match1[:bout_number]}"
|
||||||
|
match_change[:loser2_name] = "Loser of #{match2[:bout_number]}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def four_pools_to_quarter_loser_rows(rows)
|
||||||
|
quarters = rows.select { |m| m[:bracket_position] == "Quarter" }
|
||||||
|
conso_semis = rows.select { |m| m[:bracket_position] == "Conso Semis" }
|
||||||
|
semis = rows.select { |m| m[:bracket_position] == "Semis" }
|
||||||
|
third_fourth = rows.find { |m| m[:bracket_position] == "3/4" }
|
||||||
|
seventh_eighth = rows.find { |m| m[:bracket_position] == "7/8" }
|
||||||
|
|
||||||
|
conso_semis.each do |m|
|
||||||
|
if m[:bracket_position_number] == 1
|
||||||
|
q1 = quarters.find { |q| q[:bracket_position_number] == 1 }
|
||||||
|
q2 = quarters.find { |q| q[:bracket_position_number] == 2 }
|
||||||
|
m[:loser1_name] = "Loser of #{q1[:bout_number]}" if q1
|
||||||
|
m[:loser2_name] = "Loser of #{q2[:bout_number]}" if q2
|
||||||
|
elsif m[:bracket_position_number] == 2
|
||||||
|
q3 = quarters.find { |q| q[:bracket_position_number] == 3 }
|
||||||
|
q4 = quarters.find { |q| q[:bracket_position_number] == 4 }
|
||||||
|
m[:loser1_name] = "Loser of #{q3[:bout_number]}" if q3
|
||||||
|
m[:loser2_name] = "Loser of #{q4[:bout_number]}" if q4
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def assignLoserNamesWeight(weight)
|
if third_fourth
|
||||||
matches_by_weight = @tournament.matches.where(weight_id: weight.id)
|
s1 = semis.find { |s| s[:bracket_position_number] == 1 }
|
||||||
if weight.pool_bracket_type == "twoPoolsToSemi"
|
s2 = semis.find { |s| s[:bracket_position_number] == 2 }
|
||||||
twoPoolsToSemiLoser(matches_by_weight)
|
third_fourth[:loser1_name] = "Loser of #{s1[:bout_number]}" if s1
|
||||||
elsif (weight.pool_bracket_type == "fourPoolsToQuarter") or (weight.pool_bracket_type == "eightPoolsToQuarter")
|
third_fourth[:loser2_name] = "Loser of #{s2[:bout_number]}" if s2
|
||||||
fourPoolsToQuarterLoser(matches_by_weight)
|
|
||||||
elsif weight.pool_bracket_type == "fourPoolsToSemi"
|
|
||||||
fourPoolsToSemiLoser(matches_by_weight)
|
|
||||||
end
|
|
||||||
saveMatches(matches_by_weight)
|
|
||||||
end
|
|
||||||
|
|
||||||
def assignLoserNames
|
|
||||||
matches_by_weight = nil
|
|
||||||
@tournament.weights.each do |w|
|
|
||||||
matches_by_weight = @tournament.matches.where(weight_id: w.id)
|
|
||||||
if w.pool_bracket_type == "twoPoolsToSemi"
|
|
||||||
twoPoolsToSemiLoser(matches_by_weight)
|
|
||||||
elsif (w.pool_bracket_type == "fourPoolsToQuarter") or (w.pool_bracket_type == "eightPoolsToQuarter")
|
|
||||||
fourPoolsToQuarterLoser(matches_by_weight)
|
|
||||||
elsif w.pool_bracket_type == "fourPoolsToSemi"
|
|
||||||
fourPoolsToSemiLoser(matches_by_weight)
|
|
||||||
end
|
|
||||||
saveMatches(matches_by_weight)
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def twoPoolsToSemiLoser(matches_by_weight)
|
if seventh_eighth
|
||||||
match1 = matches_by_weight.select{|m| m.loser1_name == "Winner Pool 1"}.first
|
c1 = conso_semis.find { |c| c[:bracket_position_number] == 1 }
|
||||||
match2 = matches_by_weight.select{|m| m.loser1_name == "Winner Pool 2"}.first
|
c2 = conso_semis.find { |c| c[:bracket_position_number] == 2 }
|
||||||
matchChange = matches_by_weight.select{|m| m.bracket_position == "3/4"}.first
|
seventh_eighth[:loser1_name] = "Loser of #{c1[:bout_number]}" if c1
|
||||||
matchChange.loser1_name = "Loser of #{match1.bout_number}"
|
seventh_eighth[:loser2_name] = "Loser of #{c2[:bout_number]}" if c2
|
||||||
matchChange.loser2_name = "Loser of #{match2.bout_number}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def fourPoolsToQuarterLoser(matches_by_weight)
|
|
||||||
quarters = matches_by_weight.select{|m| m.bracket_position == "Quarter"}
|
|
||||||
consoSemis = matches_by_weight.select{|m| m.bracket_position == "Conso Semis"}
|
|
||||||
semis = matches_by_weight.select{|m| m.bracket_position == "Semis"}
|
|
||||||
thirdFourth = matches_by_weight.select{|m| m.bracket_position == "3/4"}.first
|
|
||||||
seventhEighth = matches_by_weight.select{|m| m.bracket_position == "7/8"}.first
|
|
||||||
consoSemis.each do |m|
|
|
||||||
if m.bracket_position_number == 1
|
|
||||||
m.loser1_name = "Loser of #{quarters.select{|m| m.bracket_position_number == 1}.first.bout_number}"
|
|
||||||
m.loser2_name = "Loser of #{quarters.select{|m| m.bracket_position_number == 2}.first.bout_number}"
|
|
||||||
elsif m.bracket_position_number == 2
|
|
||||||
m.loser1_name = "Loser of #{quarters.select{|m| m.bracket_position_number == 3}.first.bout_number}"
|
|
||||||
m.loser2_name = "Loser of #{quarters.select{|m| m.bracket_position_number == 4}.first.bout_number}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
thirdFourth.loser1_name = "Loser of #{semis.select{|m| m.bracket_position_number == 1}.first.bout_number}"
|
|
||||||
thirdFourth.loser2_name = "Loser of #{semis.select{|m| m.bracket_position_number == 2}.first.bout_number}"
|
|
||||||
consoSemis = matches_by_weight.select{|m| m.bracket_position == "Conso Semis"}
|
|
||||||
seventhEighth.loser1_name = "Loser of #{consoSemis.select{|m| m.bracket_position_number == 1}.first.bout_number}"
|
|
||||||
seventhEighth.loser2_name = "Loser of #{consoSemis.select{|m| m.bracket_position_number == 2}.first.bout_number}"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def fourPoolsToSemiLoser(matches_by_weight)
|
def four_pools_to_semi_loser_rows(rows)
|
||||||
semis = matches_by_weight.select{|m| m.bracket_position == "Semis"}
|
semis = rows.select { |m| m[:bracket_position] == "Semis" }
|
||||||
thirdFourth = matches_by_weight.select{|m| m.bracket_position == "3/4"}.first
|
conso_semis = rows.select { |m| m[:bracket_position] == "Conso Semis" }
|
||||||
consoSemis = matches_by_weight.select{|m| m.bracket_position == "Conso Semis"}
|
third_fourth = rows.find { |m| m[:bracket_position] == "3/4" }
|
||||||
seventhEighth = matches_by_weight.select{|m| m.bracket_position == "7/8"}.first
|
seventh_eighth = rows.find { |m| m[:bracket_position] == "7/8" }
|
||||||
thirdFourth.loser1_name = "Loser of #{semis.select{|m| m.bracket_position_number == 1}.first.bout_number}"
|
|
||||||
thirdFourth.loser2_name = "Loser of #{semis.select{|m| m.bracket_position_number == 2}.first.bout_number}"
|
|
||||||
seventhEighth.loser1_name = "Loser of #{consoSemis.select{|m| m.bracket_position_number == 1}.first.bout_number}"
|
|
||||||
seventhEighth.loser2_name = "Loser of #{consoSemis.select{|m| m.bracket_position_number == 2}.first.bout_number}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def saveMatches(matches)
|
if third_fourth
|
||||||
matches.each do |m|
|
s1 = semis.find { |s| s[:bracket_position_number] == 1 }
|
||||||
m.save!
|
s2 = semis.find { |s| s[:bracket_position_number] == 2 }
|
||||||
end
|
third_fourth[:loser1_name] = "Loser of #{s1[:bout_number]}" if s1
|
||||||
end
|
third_fourth[:loser2_name] = "Loser of #{s2[:bout_number]}" if s2
|
||||||
|
end
|
||||||
|
|
||||||
|
if seventh_eighth
|
||||||
|
c1 = conso_semis.find { |c| c[:bracket_position_number] == 1 }
|
||||||
|
c2 = conso_semis.find { |c| c[:bracket_position_number] == 2 }
|
||||||
|
seventh_eighth[:loser1_name] = "Loser of #{c1[:bout_number]}" if c1
|
||||||
|
seventh_eighth[:loser2_name] = "Loser of #{c2[:bout_number]}" if c2
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -1,41 +1,89 @@
|
|||||||
class PoolToBracketMatchGeneration
|
class PoolToBracketMatchGeneration
|
||||||
def initialize( tournament )
|
def initialize(tournament, weights: nil, wrestlers_by_weight_id: nil)
|
||||||
@tournament = tournament
|
@tournament = tournament
|
||||||
|
@weights = weights
|
||||||
|
@wrestlers_by_weight_id = wrestlers_by_weight_id
|
||||||
end
|
end
|
||||||
|
|
||||||
def generatePoolToBracketMatches
|
def generatePoolToBracketMatches
|
||||||
@tournament.weights.order(:max).each do |weight|
|
rows = []
|
||||||
PoolGeneration.new(weight).generatePools()
|
generation_weights.each do |weight|
|
||||||
last_match = @tournament.matches.where(weight: weight).order(round: :desc).limit(1).first
|
wrestlers = wrestlers_for_weight(weight)
|
||||||
highest_round = last_match.round
|
pool_rows = PoolGeneration.new(weight, wrestlers: wrestlers).generatePools
|
||||||
PoolBracketGeneration.new(weight, highest_round).generateBracketMatches()
|
rows.concat(pool_rows)
|
||||||
|
|
||||||
|
highest_round = pool_rows.map { |row| row[:round] }.max || 0
|
||||||
|
bracket_rows = PoolBracketGeneration.new(weight, highest_round).generateBracketMatches
|
||||||
|
rows.concat(bracket_rows)
|
||||||
end
|
end
|
||||||
movePoolSeedsToFinalPoolRound
|
|
||||||
|
movePoolSeedsToFinalPoolRound(rows)
|
||||||
|
rows
|
||||||
end
|
end
|
||||||
|
|
||||||
def movePoolSeedsToFinalPoolRound
|
def movePoolSeedsToFinalPoolRound(match_rows)
|
||||||
@tournament.weights.each do |w|
|
generation_weights.each do |w|
|
||||||
setOriginalSeedsToWrestleLastPoolRound(w)
|
setOriginalSeedsToWrestleLastPoolRound(w, match_rows)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def setOriginalSeedsToWrestleLastPoolRound(weight)
|
def setOriginalSeedsToWrestleLastPoolRound(weight, match_rows)
|
||||||
pool = 1
|
pool = 1
|
||||||
until pool > weight.pools
|
wrestlers = wrestlers_for_weight(weight)
|
||||||
wrestler1 = weight.pool_wrestlers_sorted_by_bracket_line(pool).first
|
weight_pools = weight.pools
|
||||||
wrestler2 = weight.pool_wrestlers_sorted_by_bracket_line(pool).second
|
until pool > weight_pools
|
||||||
match = wrestler1.pool_matches.sort_by{|m| m.round}.last
|
pool_wrestlers = wrestlers.select { |w| w.pool == pool }.sort_by(&:bracket_line)
|
||||||
if match.w1 != wrestler2.id or match.w2 != wrestler2.id
|
wrestler1 = pool_wrestlers.first
|
||||||
if match.w1 == wrestler1.id
|
wrestler2 = pool_wrestlers.second
|
||||||
SwapWrestlers.new.swap_wrestlers_bracket_lines(match.w2,wrestler2.id)
|
if wrestler1 && wrestler2
|
||||||
elsif match.w2 == wrestler1.id
|
pool_matches = match_rows.select { |row| row[:weight_id] == weight.id && row[:bracket_position] == "Pool" && (row[:w1] == wrestler1.id || row[:w2] == wrestler1.id) }
|
||||||
SwapWrestlers.new.swap_wrestlers_bracket_lines(match.w1,wrestler2.id)
|
match = pool_matches.max_by { |row| row[:round] }
|
||||||
end
|
if match && (match[:w1] != wrestler2.id || match[:w2] != wrestler2.id)
|
||||||
|
if match[:w1] == wrestler1.id
|
||||||
|
swap_wrestlers_in_memory(match_rows, wrestlers, match[:w2], wrestler2.id)
|
||||||
|
elsif match[:w2] == wrestler1.id
|
||||||
|
swap_wrestlers_in_memory(match_rows, wrestlers, match[:w1], wrestler2.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
pool += 1
|
pool += 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def swap_wrestlers_in_memory(match_rows, wrestlers, wrestler1_id, wrestler2_id)
|
||||||
|
w1 = wrestlers.find { |w| w.id == wrestler1_id }
|
||||||
|
w2 = wrestlers.find { |w| w.id == wrestler2_id }
|
||||||
|
return unless w1 && w2
|
||||||
|
|
||||||
|
w1_bracket_line, w1_pool = w1.bracket_line, w1.pool
|
||||||
|
w1.bracket_line, w1.pool = w2.bracket_line, w2.pool
|
||||||
|
w2.bracket_line, w2.pool = w1_bracket_line, w1_pool
|
||||||
|
|
||||||
|
swap_match_rows(match_rows, wrestler1_id, wrestler2_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def swap_match_rows(match_rows, wrestler1_id, wrestler2_id)
|
||||||
|
match_rows.each do |row|
|
||||||
|
row[:w1] = swap_id(row[:w1], wrestler1_id, wrestler2_id)
|
||||||
|
row[:w2] = swap_id(row[:w2], wrestler1_id, wrestler2_id)
|
||||||
|
row[:winner_id] = swap_id(row[:winner_id], wrestler1_id, wrestler2_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def swap_id(value, wrestler1_id, wrestler2_id)
|
||||||
|
return wrestler2_id if value == wrestler1_id
|
||||||
|
return wrestler1_id if value == wrestler2_id
|
||||||
|
|
||||||
|
value
|
||||||
|
end
|
||||||
|
|
||||||
|
def generation_weights
|
||||||
|
@weights || @tournament.weights.order(:max).to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def wrestlers_for_weight(weight)
|
||||||
|
@wrestlers_by_weight_id&.fetch(weight.id, nil) || weight.wrestlers.to_a
|
||||||
|
end
|
||||||
|
|
||||||
def assignLoserNames
|
def assignLoserNames
|
||||||
PoolToBracketGenerateLoserNames.new(@tournament).assignLoserNames
|
PoolToBracketGenerateLoserNames.new(@tournament).assignLoserNames
|
||||||
|
|||||||
@@ -3,16 +3,22 @@ class TournamentSeeding
|
|||||||
@tournament = tournament
|
@tournament = tournament
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_seeds
|
def set_seeds(weights: nil, persist: true)
|
||||||
@tournament.weights.each do |weight|
|
weights_to_seed = weights || @tournament.weights.includes(:wrestlers)
|
||||||
|
updated_wrestlers = []
|
||||||
|
|
||||||
|
weights_to_seed.each do |weight|
|
||||||
wrestlers = weight.wrestlers
|
wrestlers = weight.wrestlers
|
||||||
bracket_size = weight.calculate_bracket_size
|
bracket_size = weight.calculate_bracket_size
|
||||||
|
|
||||||
wrestlers = reset_bracket_line_for_lines_higher_than_bracket_size(wrestlers, bracket_size)
|
wrestlers = reset_bracket_line_for_lines_higher_than_bracket_size(wrestlers, bracket_size)
|
||||||
wrestlers = set_original_seed_to_bracket_line(wrestlers)
|
wrestlers = set_original_seed_to_bracket_line(wrestlers)
|
||||||
wrestlers = random_seeding(wrestlers, bracket_size)
|
wrestlers = random_seeding(wrestlers, bracket_size)
|
||||||
wrestlers.each(&:save)
|
updated_wrestlers.concat(wrestlers)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
persist_bracket_lines(updated_wrestlers) if persist
|
||||||
|
updated_wrestlers
|
||||||
end
|
end
|
||||||
|
|
||||||
def random_seeding(wrestlers, bracket_size)
|
def random_seeding(wrestlers, bracket_size)
|
||||||
@@ -96,4 +102,19 @@ class TournamentSeeding
|
|||||||
end
|
end
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def persist_bracket_lines(wrestlers)
|
||||||
|
return if wrestlers.blank?
|
||||||
|
|
||||||
|
timestamp = Time.current
|
||||||
|
updates = wrestlers.map do |wrestler|
|
||||||
|
{
|
||||||
|
id: wrestler.id,
|
||||||
|
bracket_line: wrestler.bracket_line,
|
||||||
|
updated_at: timestamp
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
Wrestler.upsert_all(updates)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -14,7 +14,7 @@ class WipeTournamentMatches
|
|||||||
end
|
end
|
||||||
|
|
||||||
def wipeMatches
|
def wipeMatches
|
||||||
@tournament.matches.destroy_all
|
@tournament.destroy_all_matches
|
||||||
end
|
end
|
||||||
|
|
||||||
def resetSchoolScores
|
def resetSchoolScores
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ class GeneratePoolNumbers
|
|||||||
@weight = weight
|
@weight = weight
|
||||||
end
|
end
|
||||||
|
|
||||||
def savePoolNumbers
|
def savePoolNumbers(wrestlers: nil, persist: true)
|
||||||
@weight.wrestlers.each do |wrestler|
|
wrestlers_to_update = wrestlers || @weight.wrestlers.to_a
|
||||||
|
wrestlers_to_update.each do |wrestler|
|
||||||
wrestler.pool = get_wrestler_pool_number(@weight.pools, wrestler.bracket_line)
|
wrestler.pool = get_wrestler_pool_number(@weight.pools, wrestler.bracket_line)
|
||||||
wrestler.save
|
|
||||||
end
|
end
|
||||||
|
persist_pool_numbers(wrestlers_to_update) if persist
|
||||||
|
wrestlers_to_update
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_wrestler_pool_number(number_of_pools, wrestler_seed)
|
def get_wrestler_pool_number(number_of_pools, wrestler_seed)
|
||||||
@@ -36,4 +38,20 @@ class GeneratePoolNumbers
|
|||||||
|
|
||||||
pool
|
pool
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def persist_pool_numbers(wrestlers)
|
||||||
|
return if wrestlers.blank?
|
||||||
|
|
||||||
|
timestamp = Time.current
|
||||||
|
rows = wrestlers.map do |w|
|
||||||
|
{
|
||||||
|
id: w.id,
|
||||||
|
pool: w.pool,
|
||||||
|
updated_at: timestamp
|
||||||
|
}
|
||||||
|
end
|
||||||
|
Wrestler.upsert_all(rows)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -54,29 +54,20 @@ class CalculateWrestlerTeamScore
|
|||||||
def byePoints
|
def byePoints
|
||||||
points = 0
|
points = 0
|
||||||
if @tournament.tournament_type == "Pool to bracket"
|
if @tournament.tournament_type == "Pool to bracket"
|
||||||
if @wrestler.pool_wins.size >= 1 and @wrestler.has_a_pool_bye == true
|
if pool_bye_points_eligible?
|
||||||
points += 2
|
points += 2
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if @tournament.tournament_type.include? "Regular Double Elimination"
|
if @tournament.tournament_type.include? "Double Elimination"
|
||||||
if @wrestler.championship_advancement_wins.size > 0 or @wrestler.matches_won.select{|m| m.bracket_position == "1/2" and m.win_type != "BYE"}.size > 0
|
if @wrestler.championship_advancement_wins.any? &&
|
||||||
# if they have a win in the championship round or if they got a bye all the way to finals and won the finals
|
@wrestler.championship_byes.any? &&
|
||||||
points += @wrestler.championship_byes.size * 2
|
any_bye_round_had_wrestled_match?(@wrestler.championship_byes)
|
||||||
|
points += 2
|
||||||
end
|
end
|
||||||
if @wrestler.consolation_advancement_wins.size > 0 or @wrestler.matches_won.select{|m| m.bracket_position == "3/4" and m.win_type != "BYE"}.size > 0
|
if @wrestler.consolation_advancement_wins.any? &&
|
||||||
# if they have a win in the consolation round or if they got a bye all the way to 3rd/4th match and won
|
@wrestler.consolation_byes.any? &&
|
||||||
points += @wrestler.consolation_byes.size * 1
|
any_bye_round_had_wrestled_match?(@wrestler.consolation_byes)
|
||||||
end
|
points += 1
|
||||||
end
|
|
||||||
if @tournament.tournament_type.include? "Modified 16 Man Double Elimination"
|
|
||||||
if @wrestler.championship_advancement_wins.size > 0 or @wrestler.matches_won.select{|m| m.bracket_position == "1/2" and m.win_type != "BYE"}.size > 0
|
|
||||||
# if they have a win in the championship round or if they got a bye all the way to finals and won the finals
|
|
||||||
points += @wrestler.championship_byes.size * 2
|
|
||||||
end
|
|
||||||
if @wrestler.consolation_advancement_wins.size > 0 or @wrestler.matches_won.select{|m| m.bracket_position == "5/6" and m.win_type != "BYE"}.size > 0
|
|
||||||
# if they have a win in the consolation round or if they got a bye all the way to 5th/6th match and won
|
|
||||||
# since the consolation bracket goes to 5/6 in a modified tournament
|
|
||||||
points += @wrestler.consolation_byes.size * 1
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return points
|
return points
|
||||||
@@ -86,4 +77,30 @@ class CalculateWrestlerTeamScore
|
|||||||
(@wrestler.pin_wins.size * 2) + (@wrestler.tech_wins.size * 1.5) + (@wrestler.major_wins.size * 1)
|
(@wrestler.pin_wins.size * 2) + (@wrestler.tech_wins.size * 1.5) + (@wrestler.major_wins.size * 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def pool_bye_points_eligible?
|
||||||
|
return false unless @wrestler.pool_wins.size >= 1
|
||||||
|
return false unless @wrestler.weight.pools.to_i > 1
|
||||||
|
|
||||||
|
wrestler_pool_size = @wrestler.weight.wrestlers_in_pool(@wrestler.pool).size
|
||||||
|
largest_pool_size = (1..@wrestler.weight.pools).map { |pool_number| @wrestler.weight.wrestlers_in_pool(pool_number).size }.max
|
||||||
|
|
||||||
|
wrestler_pool_size < largest_pool_size
|
||||||
|
end
|
||||||
|
|
||||||
|
def any_bye_round_had_wrestled_match?(bye_matches)
|
||||||
|
bye_matches.any? do |bye_match|
|
||||||
|
next false if bye_match.round.nil?
|
||||||
|
|
||||||
|
@wrestler.weight.matches.any? do |match|
|
||||||
|
next false if match.id == bye_match.id
|
||||||
|
next false if match.round != bye_match.round
|
||||||
|
next false if match.is_consolation_match != bye_match.is_consolation_match
|
||||||
|
|
||||||
|
match.finished == 1 && match.win_type.present? && match.win_type != "BYE"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li><%= link_to " Bout Board" , "/tournaments/#{@tournament.id}/up_matches", class: "fas fa-list-alt" %></li>
|
<li><%= link_to " Bout Board" , "/tournaments/#{@tournament.id}/up_matches", class: "fas fa-list-alt" %></li>
|
||||||
|
<li><%= link_to " Live Scores" , "/tournaments/#{@tournament.id}/live_scores", class: "fas fa-tv" %></li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if can? :manage, @tournament %>
|
<% if can? :manage, @tournament %>
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
|
|||||||
70
app/views/matches/_match_results_fields.html.erb
Normal file
70
app/views/matches/_match_results_fields.html.erb
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<% submit_label = local_assigns.fetch(:submit_label, "Update Match") %>
|
||||||
|
<% redirect_path = local_assigns[:redirect_path] %>
|
||||||
|
|
||||||
|
<h4>Match Results</h4>
|
||||||
|
<br>
|
||||||
|
<div data-controller="match-score" data-match-score-finished-value="<%= @match.finished == 1 %>">
|
||||||
|
<div class="field">
|
||||||
|
<%= f.label "Win type" %><br>
|
||||||
|
<%= f.select :win_type, Match::WIN_TYPES, { include_blank: false }, {
|
||||||
|
data: {
|
||||||
|
match_score_target: "winType",
|
||||||
|
action: "change->match-score#winTypeChanged"
|
||||||
|
}
|
||||||
|
} %>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="field">
|
||||||
|
<%= f.label "Overtime Type" %> Leave blank if not overtime. For High School the 1st overtime is SV-1, second overtime is TB-1, third overtime is UTB.<br>
|
||||||
|
<%= f.select(:overtime_type, Match::OVERTIME_TYPES, {}, {
|
||||||
|
data: {
|
||||||
|
match_score_target: "overtimeSelect"
|
||||||
|
}
|
||||||
|
}) %>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="field">
|
||||||
|
<%= f.label "Winner" %> Please choose the winner<br>
|
||||||
|
<%= f.collection_select :winner_id, @wrestlers, :id, :name_with_school,
|
||||||
|
{ include_blank: true },
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
match_score_target: "winnerSelect",
|
||||||
|
action: "change->match-score#winnerChanged"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
%>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="field">
|
||||||
|
<%= f.label "Final Score" %>
|
||||||
|
<br>
|
||||||
|
<% if @match.finished == 1 %>
|
||||||
|
<%= f.text_field :score, id: "final-score-field", data: { match_score_target: "finalScoreField" }, class: "form-control", style: "max-width: 220px;" %>
|
||||||
|
<% else %>
|
||||||
|
<span id="score-help-text">
|
||||||
|
The input will adjust based on the selected win type.
|
||||||
|
</span>
|
||||||
|
<br>
|
||||||
|
<div id="dynamic-score-input" data-match-score-target="dynamicScoreInput"></div>
|
||||||
|
<p id="pin-time-tip" class="text-muted mt-2" style="display: none;" data-match-score-target="pinTimeTip">
|
||||||
|
<strong>Tip:</strong> Pin time is an accumulation over the match, not how much time was left in the current period.
|
||||||
|
<br>For example, if all 3 periods are 2 minutes and a pin happened with 1:27 left in the second period,
|
||||||
|
the pin time would be <strong>2:33</strong> (2 minutes for the first period + 33 seconds elapsed in the second period).
|
||||||
|
</p>
|
||||||
|
<%= f.hidden_field :score, id: "final-score-field", data: { match_score_target: "finalScoreField" } %>
|
||||||
|
<% end %>
|
||||||
|
<div id="validation-alerts" class="text-danger mt-2" data-match-score-target="validationAlerts"></div>
|
||||||
|
<%= hidden_field_tag :redirect_to, redirect_path if redirect_path.present? %>
|
||||||
|
<%= f.hidden_field :finished, value: 1 %>
|
||||||
|
<%= f.hidden_field :round, value: @match.round %>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<%= f.submit submit_label, id: "update-match-btn",
|
||||||
|
data: {
|
||||||
|
match_score_target: "submitButton",
|
||||||
|
action: "click->match-score#confirmWinner"
|
||||||
|
},
|
||||||
|
class: "btn btn-success" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -18,8 +18,19 @@
|
|||||||
|
|
||||||
<div id="cable-status-indicator" data-match-data-target="statusIndicator" class="alert alert-secondary" style="padding: 5px; margin-bottom: 10px; border-radius: 4px;"></div>
|
<div id="cable-status-indicator" data-match-data-target="statusIndicator" class="alert alert-secondary" style="padding: 5px; margin-bottom: 10px; border-radius: 4px;"></div>
|
||||||
<h4>Bout <strong><%= @match.bout_number %></strong></h4>
|
<h4>Bout <strong><%= @match.bout_number %></strong></h4>
|
||||||
<% if @show_next_bout_button && @next_match %>
|
<% if @mat %>
|
||||||
<%= link_to "Skip to Next Match for Mat #{@mat.name}", mat_path(@mat, bout_number: @next_match.bout_number), class: "btn btn-primary" %>
|
<% queue_matches = @queue_matches || @mat.queue_matches %>
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<% queue_matches.each_with_index do |queue_match, index| %>
|
||||||
|
<% queue_label = "Queue #{index + 1}" %>
|
||||||
|
<% if queue_match %>
|
||||||
|
<% button_class = queue_match.id == @match.id ? "btn btn-success btn-sm" : "btn btn-primary btn-sm" %>
|
||||||
|
<%= link_to "#{queue_label}: Bout #{queue_match.bout_number}", mat_path(@mat, bout_number: queue_match.bout_number), class: button_class %>
|
||||||
|
<% else %>
|
||||||
|
<button type="button" class="btn btn-default btn-sm" disabled><%= "#{queue_label}: Not assigned" %></button>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<h4>Bracket Position: <strong><%= @match.bracket_position %></strong></h4>
|
<h4>Bracket Position: <strong><%= @match.bracket_position %></strong></h4>
|
||||||
|
|
||||||
@@ -119,65 +130,6 @@
|
|||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<h4>Match Results</h4>
|
<%= render "matches/match_results_fields", f: f, redirect_path: @match_results_redirect_path %>
|
||||||
<br>
|
|
||||||
<div data-controller="match-score">
|
|
||||||
<div class="field">
|
|
||||||
<%= f.label "Win type" %><br>
|
|
||||||
<%= f.select :win_type, Match::WIN_TYPES, { include_blank: false }, {
|
|
||||||
data: {
|
|
||||||
match_score_target: "winType",
|
|
||||||
action: "change->match-score#winTypeChanged"
|
|
||||||
}
|
|
||||||
} %>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<div class="field">
|
|
||||||
<%= f.label "Overtime Type" %> Leave blank if not overtime. For High School the 1st overtime is SV-1, second overtime is TB-1, third overtime is UTB.<br>
|
|
||||||
<%= f.select(:overtime_type, Match::OVERTIME_TYPES) %>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<div class="field">
|
|
||||||
<%= f.label "Winner" %> Please choose the winner<br>
|
|
||||||
<%= f.collection_select :winner_id, @wrestlers, :id, :name_with_school,
|
|
||||||
{ include_blank: true },
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
match_score_target: "winnerSelect",
|
|
||||||
action: "change->match-score#winnerChanged"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
%>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<div class="field">
|
|
||||||
<%= f.label "Final Score" %>
|
|
||||||
<br>
|
|
||||||
<span id="score-help-text">
|
|
||||||
The input will adjust based on the selected win type.
|
|
||||||
</span>
|
|
||||||
<br>
|
|
||||||
<div id="dynamic-score-input" data-match-score-target="dynamicScoreInput"></div>
|
|
||||||
<p id="pin-time-tip" class="text-muted mt-2" style="display: none;" data-match-score-target="pinTimeTip">
|
|
||||||
<strong>Tip:</strong> Pin time is an accumulation over the match, not how much time was left in the current period.
|
|
||||||
<br>For example, if all 3 periods are 2 minutes and a pin happened with 1:27 left in the second period,
|
|
||||||
the pin time would be <strong>2:33</strong> (2 minutes for the first period + 33 seconds elapsed in the second period).
|
|
||||||
</p>
|
|
||||||
<div id="validation-alerts" class="text-danger mt-2" data-match-score-target="validationAlerts"></div>
|
|
||||||
<%= f.hidden_field :score, id: "final-score-field", data: { match_score_target: "finalScoreField" } %>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
<%= f.submit "Update Match", id: "update-match-btn",
|
|
||||||
data: {
|
|
||||||
match_score_target: "submitButton",
|
|
||||||
action: "click->match-score#confirmWinner"
|
|
||||||
},
|
|
||||||
class: "btn btn-success" %>
|
|
||||||
</div>
|
|
||||||
</div><!-- End of match-score controller -->
|
|
||||||
</div><!-- End of match-data controller div -->
|
</div><!-- End of match-data controller div -->
|
||||||
|
|
||||||
<br>
|
|
||||||
<%= f.hidden_field :finished, :value => 1 %>
|
|
||||||
<%= f.hidden_field :round, :value => @match.round %>
|
|
||||||
<% end %><!-- End of form_for -->
|
<% end %><!-- End of form_for -->
|
||||||
|
|||||||
113
app/views/matches/_scoreboard.html.erb
Normal file
113
app/views/matches/_scoreboard.html.erb
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<%
|
||||||
|
source_mode = local_assigns.fetch(:source_mode, "localstorage")
|
||||||
|
display_mode = local_assigns.fetch(:display_mode, "fullscreen")
|
||||||
|
show_mat_banner = local_assigns.fetch(:show_mat_banner, false)
|
||||||
|
show_stats = local_assigns.fetch(:show_stats, false)
|
||||||
|
mat = local_assigns[:mat]
|
||||||
|
match = local_assigns[:match]
|
||||||
|
tournament = local_assigns[:tournament] || match&.tournament
|
||||||
|
|
||||||
|
fullscreen = display_mode == "fullscreen"
|
||||||
|
root_style = if fullscreen
|
||||||
|
"min-height: 100vh; background: #000; color: #fff; display: flex; align-items: stretch; justify-content: stretch; padding: 0; margin: 0; position: relative;"
|
||||||
|
elsif show_stats
|
||||||
|
"background: #000; color: #fff; padding: 0; margin: 0; position: relative; width: 100%; border: 1px solid #222;"
|
||||||
|
else
|
||||||
|
"background: #000; color: #fff; display: flex; align-items: stretch; justify-content: stretch; padding: 0; margin: 0; position: relative; width: 100%; min-height: 260px; border: 1px solid #222;"
|
||||||
|
end
|
||||||
|
inner_style = if fullscreen
|
||||||
|
"display: grid; grid-template-rows: auto 1fr; width: 100vw; min-height: 100vh;"
|
||||||
|
elsif show_stats
|
||||||
|
"display: grid; grid-template-rows: auto auto auto; width: 100%;"
|
||||||
|
else
|
||||||
|
"display: grid; grid-template-rows: auto 1fr; width: 100%; min-height: 260px;"
|
||||||
|
end
|
||||||
|
board_style = fullscreen ? "display: grid; grid-template-columns: 1fr 1.3fr 1fr; width: 100%; min-height: 0;" : "display: grid; grid-template-columns: 1fr 1.2fr 1fr; width: 100%; min-height: 0; min-height: 260px;"
|
||||||
|
panel_padding = fullscreen ? "2vw" : "1rem"
|
||||||
|
name_size = fullscreen ? "clamp(2rem, 4vw, 4rem)" : "clamp(1rem, 2vw, 1.8rem)"
|
||||||
|
school_size = fullscreen ? "clamp(1rem, 2vw, 2rem)" : "clamp(0.85rem, 1.3vw, 1.1rem)"
|
||||||
|
score_size = fullscreen ? "clamp(8rem, 18vw, 16rem)" : "clamp(3rem, 8vw, 6rem)"
|
||||||
|
period_size = fullscreen ? "clamp(1.5rem, 3vw, 2.5rem)" : "clamp(1rem, 2vw, 1.5rem)"
|
||||||
|
clock_size = fullscreen ? "clamp(6rem, 16vw, 14rem)" : "clamp(3rem, 10vw, 5.5rem)"
|
||||||
|
banner_offset = fullscreen ? "6vw" : "2rem"
|
||||||
|
banner_border = fullscreen ? "0.45vw" : "4px"
|
||||||
|
center_border = fullscreen ? "1vw" : "6px"
|
||||||
|
%>
|
||||||
|
|
||||||
|
<div
|
||||||
|
data-controller="match-scoreboard"
|
||||||
|
data-match-scoreboard-source-mode-value="<%= source_mode %>"
|
||||||
|
data-match-scoreboard-display-mode-value="<%= display_mode %>"
|
||||||
|
data-match-scoreboard-match-id-value="<%= match&.id || 0 %>"
|
||||||
|
data-match-scoreboard-mat-id-value="<%= mat&.id || 0 %>"
|
||||||
|
data-match-scoreboard-tournament-id-value="<%= tournament&.id || 0 %>"
|
||||||
|
data-match-scoreboard-initial-bout-number-value="<%= match&.bout_number || 0 %>"
|
||||||
|
style="<%= root_style %>">
|
||||||
|
<div style="<%= inner_style %>">
|
||||||
|
<% if show_mat_banner %>
|
||||||
|
<div style="background: #111; color: #fff; text-align: center; padding: 1vh 2vw; border-bottom: 0.5vw solid #fff;">
|
||||||
|
<div style="font-size: clamp(1.5rem, 3vw, 3rem); font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;">
|
||||||
|
Mat <%= mat&.name %>
|
||||||
|
</div>
|
||||||
|
<div data-match-scoreboard-target="boutLabel" style="font-size: clamp(1rem, 2vw, 1.75rem); letter-spacing: 0.08em; text-transform: uppercase; opacity: 0.95; margin-top: 0.3rem;">
|
||||||
|
Bout <%= match&.bout_number || "" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<div style="background: #111; color: #fff; text-align: center; padding: 0.35rem 1rem; border-bottom: 4px solid #fff;">
|
||||||
|
<div data-match-scoreboard-target="boutLabel" style="font-size: clamp(0.85rem, 1.3vw, 1.1rem); letter-spacing: 0.08em; text-transform: uppercase; opacity: 0.95;">
|
||||||
|
Bout <%= match&.bout_number || "" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div style="<%= board_style %>">
|
||||||
|
<section data-match-scoreboard-target="redSection" style="background: #c91f1f; display: flex; flex-direction: column; justify-content: center; align-items: center; padding: <%= panel_padding %>; text-align: center;">
|
||||||
|
<div data-match-scoreboard-target="redName" style="font-size: <%= name_size %>; font-weight: 700; line-height: 1;">NO MATCH</div>
|
||||||
|
<div data-match-scoreboard-target="redSchool" style="font-size: <%= school_size %>; margin-top: 0.75rem; opacity: 0.95;"></div>
|
||||||
|
<div data-match-scoreboard-target="redTimerIndicator" style="font-size: <%= school_size %>; margin-top: 0.75rem;"></div>
|
||||||
|
<div data-match-scoreboard-target="redScore" style="font-size: <%= score_size %>; font-weight: 800; line-height: 0.9; margin-top: 3vh;">0</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section data-match-scoreboard-target="centerSection" style="background: #050505; display: flex; flex-direction: column; justify-content: center; align-items: center; padding: <%= panel_padding %>; text-align: center; border-left: <%= center_border %> solid #fff; border-right: <%= center_border %> solid #fff;">
|
||||||
|
<div data-match-scoreboard-target="periodLabel" style="font-size: <%= period_size %>; font-weight: 700; margin-top: 2vh; text-transform: uppercase;">No Match</div>
|
||||||
|
<div data-match-scoreboard-target="clock" style="font-size: <%= clock_size %>; font-weight: 800; line-height: 0.9; margin-top: 3vh;">-</div>
|
||||||
|
<div data-match-scoreboard-target="emptyState" style="display: none; margin-top: 4vh; font-size: clamp(1.5rem, 3vw, 4rem); font-weight: 700; color: #fff;">No Match</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section data-match-scoreboard-target="greenSection" style="background: #1cab2d; display: flex; flex-direction: column; justify-content: center; align-items: center; padding: <%= panel_padding %>; text-align: center;">
|
||||||
|
<div data-match-scoreboard-target="greenName" style="font-size: <%= name_size %>; font-weight: 700; line-height: 1;">NO MATCH</div>
|
||||||
|
<div data-match-scoreboard-target="greenSchool" style="font-size: <%= school_size %>; margin-top: 0.75rem; opacity: 0.95;"></div>
|
||||||
|
<div data-match-scoreboard-target="greenTimerIndicator" style="font-size: <%= school_size %>; margin-top: 0.75rem;"></div>
|
||||||
|
<div data-match-scoreboard-target="greenScore" style="font-size: <%= score_size %>; font-weight: 800; line-height: 0.9; margin-top: 3vh;">0</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if show_stats %>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; background: #fff; color: #111; padding: 12px;">
|
||||||
|
<section style="border: 1px solid #d9d9d9; padding: 12px; min-width: 0;">
|
||||||
|
<div style="font-weight: 700; margin-bottom: 8px;">Red Stats</div>
|
||||||
|
<pre data-match-scoreboard-target="redStats" style="margin: 0; background: #f7f7f7; border: 1px solid #ececec; padding: 10px; min-height: 120px; white-space: pre-wrap; overflow-wrap: anywhere;"></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style="border: 1px solid #d9d9d9; padding: 12px; min-width: 0;">
|
||||||
|
<div style="font-weight: 700; margin-bottom: 8px;">Green Stats</div>
|
||||||
|
<pre data-match-scoreboard-target="greenStats" style="margin: 0; background: #f7f7f7; border: 1px solid #ececec; padding: 10px; min-height: 120px; white-space: pre-wrap; overflow-wrap: anywhere;"></pre>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div style="background: #fff; color: #111; padding: 0 12px 12px;">
|
||||||
|
<section style="border: 1px solid #d9d9d9; padding: 12px;">
|
||||||
|
<div style="font-weight: 700; margin-bottom: 8px;">Last Match Result</div>
|
||||||
|
<div data-match-scoreboard-target="lastMatchResult">-</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
data-match-scoreboard-target="timerBanner"
|
||||||
|
style="display: none; position: absolute; left: <%= banner_offset %>; right: <%= banner_offset %>; top: 50%; transform: translateY(-50%); background: rgba(15, 15, 15, 0.96); border: <%= banner_border %> solid #fff; padding: 1.5vh 2vw; text-align: center; z-index: 20;">
|
||||||
|
<div data-match-scoreboard-target="timerBannerLabel" style="font-size: <%= fullscreen ? "clamp(1.3rem, 2.6vw, 2.6rem)" : "clamp(0.9rem, 1.6vw, 1.25rem)" %>; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;"></div>
|
||||||
|
<div data-match-scoreboard-target="timerBannerClock" style="font-size: <%= fullscreen ? "clamp(2.5rem, 6vw, 6rem)" : "clamp(1.6rem, 4vw, 3rem)" %>; font-weight: 800; line-height: 1; margin-top: 0.5vh;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -10,6 +10,17 @@
|
|||||||
class="alert alert-secondary"
|
class="alert alert-secondary"
|
||||||
style="padding: 5px; margin-bottom: 10px; border-radius: 4px;"></div>
|
style="padding: 5px; margin-bottom: 10px; border-radius: 4px;"></div>
|
||||||
|
|
||||||
|
<% unless @match.finished %>
|
||||||
|
<div data-match-spectate-target="scoreboardContainer" style="margin-bottom: 20px;">
|
||||||
|
<%= render "matches/scoreboard",
|
||||||
|
source_mode: "websocket",
|
||||||
|
display_mode: "embedded",
|
||||||
|
show_mat_banner: false,
|
||||||
|
match: @match,
|
||||||
|
tournament: @tournament %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div class="match-details">
|
<div class="match-details">
|
||||||
<div class="wrestler-info wrestler1">
|
<div class="wrestler-info wrestler1">
|
||||||
<h4><%= @wrestler1_name %> (<%= @wrestler1_school_name %>)</h4>
|
<h4><%= @wrestler1_name %> (<%= @wrestler1_school_name %>)</h4>
|
||||||
|
|||||||
108
app/views/matches/state.html.erb
Normal file
108
app/views/matches/state.html.erb
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<h1><%= @wrestler1_name %> VS. <%= @wrestler2_name %></h1>
|
||||||
|
|
||||||
|
<div
|
||||||
|
data-controller="match-state"
|
||||||
|
data-match-state-match-id-value="<%= @match.id %>"
|
||||||
|
data-match-state-tournament-id-value="<%= @match.tournament.id %>"
|
||||||
|
data-match-state-bout-number-value="<%= @match.bout_number %>"
|
||||||
|
data-match-state-weight-label-value="<%= @match.weight&.max %>"
|
||||||
|
data-match-state-bracket-position-value="<%= @match.bracket_position %>"
|
||||||
|
data-match-state-ruleset-value="<%= @match_state_ruleset %>"
|
||||||
|
data-match-state-w1-id-value="<%= @match.w1 || 0 %>"
|
||||||
|
data-match-state-w2-id-value="<%= @match.w2 || 0 %>"
|
||||||
|
data-match-state-w1-name-value="<%= @wrestler1_name %>"
|
||||||
|
data-match-state-w2-name-value="<%= @wrestler2_name %>"
|
||||||
|
data-match-state-w1-school-value="<%= @wrestler1_school_name %>"
|
||||||
|
data-match-state-w2-school-value="<%= @wrestler2_school_name %>">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="panel panel-success" data-match-state-target="greenPanel">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong data-match-state-target="greenLabel">Green</strong> <span data-match-state-target="greenName"><%= @wrestler1_name %></span>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p class="text-muted" data-match-state-target="greenSchool"><%= @wrestler1_school_name %></p>
|
||||||
|
<h2 data-match-state-target="greenScore">0</h2>
|
||||||
|
<div data-match-state-target="greenControls"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Bout <%= @match.bout_number %></strong>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p><strong>Bracket Position:</strong> <%= @match.bracket_position %></p>
|
||||||
|
<p><strong>Period:</strong> <span data-match-state-target="periodLabel"></span></p>
|
||||||
|
<p><strong>Clock:</strong> <span data-match-state-target="clock">2:00</span></p>
|
||||||
|
<p><strong>Status:</strong> <span data-match-state-target="clockStatus">Stopped</span></p>
|
||||||
|
<p><strong>Match Position:</strong> <span data-match-state-target="matchPosition"></span></p>
|
||||||
|
<p><strong>Format:</strong> <span data-match-state-target="formatName"></span></p>
|
||||||
|
<div class="btn-group" style="margin-top: 10px;">
|
||||||
|
<button type="button" class="btn btn-success btn-sm" data-action="click->match-state#startClock">Start</button>
|
||||||
|
<button type="button" class="btn btn-danger btn-sm" data-action="click->match-state#stopClock">Stop</button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<strong>Adjust Match Clock</strong>
|
||||||
|
<div class="btn-group" style="margin-top: 8px;">
|
||||||
|
<button type="button" class="btn btn-default btn-sm" data-action="click->match-state#subtractMinute">-1m</button>
|
||||||
|
<button type="button" class="btn btn-default btn-sm" data-action="click->match-state#subtractSecond">-1s</button>
|
||||||
|
<button type="button" class="btn btn-default btn-sm" data-action="click->match-state#addSecond">+1s</button>
|
||||||
|
<button type="button" class="btn btn-default btn-sm" data-action="click->match-state#addMinute">+1m</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 12px;">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" data-action="click->match-state#swapColors">Swap Red/Green</button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 12px;">
|
||||||
|
<strong>Match Period Navigation</strong>
|
||||||
|
<div class="text-muted">Use these to move between periods and choice periods.</div>
|
||||||
|
<div class="btn-group" style="margin-top: 8px;">
|
||||||
|
<button type="button" class="btn btn-default btn-sm" data-action="click->match-state#previousPhase">Previous Period</button>
|
||||||
|
<button type="button" class="btn btn-default btn-sm" data-action="click->match-state#nextPhase">Next Period</button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 8px;">
|
||||||
|
<strong>Match Time Accumulation:</strong>
|
||||||
|
<span data-match-state-target="accumulationClock">0:00</span>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 12px;">
|
||||||
|
<button type="button" class="btn btn-warning btn-sm" data-action="click->match-state#resetMatch">Reset Match</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div data-match-state-target="choiceActions" style="margin-top: 12px;"></div>
|
||||||
|
<hr>
|
||||||
|
<h4>Event Log</h4>
|
||||||
|
<div data-match-state-target="eventLog"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="panel panel-danger" data-match-state-target="redPanel">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong data-match-state-target="redLabel">Red</strong> <span data-match-state-target="redName"><%= @wrestler2_name %></span>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p class="text-muted" data-match-state-target="redSchool"><%= @wrestler2_school_name %></p>
|
||||||
|
<h2 data-match-state-target="redScore">0</h2>
|
||||||
|
<div data-match-state-target="redControls"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-default" data-match-state-target="matchResultsPanel">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Submit Match Results</strong>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<%= form_for(@match) do |f| %>
|
||||||
|
<%= f.hidden_field :w1_stat, data: { match_state_target: "w1StatField" } %>
|
||||||
|
<%= f.hidden_field :w2_stat, data: { match_state_target: "w2StatField" } %>
|
||||||
|
<%= render "matches/match_results_fields", f: f, redirect_path: @match_results_redirect_path, submit_label: "Update Match" %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<% @mat = mat %>
|
<% @mat = mat %>
|
||||||
<% @match = local_assigns[:match] || mat.queue1_match %>
|
<% @queue_matches = local_assigns[:queue_matches] || mat.queue_matches %>
|
||||||
<% @next_match = local_assigns[:next_match] || mat.queue2_match %>
|
<% @match = local_assigns[:match] || @queue_matches[0] %>
|
||||||
|
<% @match ||= @queue_matches[0] %>
|
||||||
|
<% @next_match = local_assigns[:next_match] || @queue_matches[1] %>
|
||||||
<% @show_next_bout_button = local_assigns.key?(:show_next_bout_button) ? local_assigns[:show_next_bout_button] : true %>
|
<% @show_next_bout_button = local_assigns.key?(:show_next_bout_button) ? local_assigns[:show_next_bout_button] : true %>
|
||||||
|
|
||||||
<% @wrestlers = [] %>
|
<% @wrestlers = [] %>
|
||||||
|
|||||||
7
app/views/mats/scoreboard.html.erb
Normal file
7
app/views/mats/scoreboard.html.erb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<%= render "matches/scoreboard",
|
||||||
|
source_mode: "localstorage",
|
||||||
|
display_mode: "fullscreen",
|
||||||
|
show_mat_banner: true,
|
||||||
|
mat: @mat,
|
||||||
|
match: @match,
|
||||||
|
tournament: @tournament %>
|
||||||
33
app/views/mats/state.html.erb
Normal file
33
app/views/mats/state.html.erb
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<% if @match %>
|
||||||
|
<div
|
||||||
|
data-controller="mat-state"
|
||||||
|
data-mat-state-tournament-id-value="<%= @tournament.id %>"
|
||||||
|
data-mat-state-mat-id-value="<%= @mat.id %>"
|
||||||
|
data-mat-state-bout-number-value="<%= @match.bout_number %>"
|
||||||
|
data-mat-state-match-id-value="<%= @match.id %>"
|
||||||
|
data-mat-state-select-match-url-value="<%= select_match_mat_path(@mat) %>"
|
||||||
|
data-mat-state-weight-label-value="<%= @match.weight&.max %>"
|
||||||
|
data-mat-state-w1-id-value="<%= @match.w1 || 0 %>"
|
||||||
|
data-mat-state-w2-id-value="<%= @match.w2 || 0 %>"
|
||||||
|
data-mat-state-w1-name-value="<%= @wrestler1_name %>"
|
||||||
|
data-mat-state-w2-name-value="<%= @wrestler2_name %>"
|
||||||
|
data-mat-state-w1-school-value="<%= @wrestler1_school_name %>"
|
||||||
|
data-mat-state-w2-school-value="<%= @wrestler2_school_name %>">
|
||||||
|
<h3>Mat <%= @mat.name %></h3>
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<% @queue_matches.each_with_index do |queue_match, index| %>
|
||||||
|
<% queue_label = "Queue #{index + 1}" %>
|
||||||
|
<% if queue_match %>
|
||||||
|
<% button_class = queue_match.id == @match.id ? "btn btn-success btn-sm" : "btn btn-primary btn-sm" %>
|
||||||
|
<%= link_to "#{queue_label}: Bout #{queue_match.bout_number}", state_mat_path(@mat, bout_number: queue_match.bout_number), class: button_class %>
|
||||||
|
<% else %>
|
||||||
|
<button type="button" class="btn btn-default btn-sm" disabled><%= "#{queue_label}: Not assigned" %></button>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<%= render template: "matches/state" %>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<h3>Mat <%= @mat.name %></h3>
|
||||||
|
<p>No matches assigned to this mat.</p>
|
||||||
|
<% end %>
|
||||||
13
app/views/schools/_wrestler_row_cells.html.erb
Normal file
13
app/views/schools/_wrestler_row_cells.html.erb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<% if local_assigns[:school_permission_key].present? %>
|
||||||
|
<% wrestler_path_with_key = wrestler_path(wrestler) %>
|
||||||
|
<% wrestler_path_with_key += "?school_permission_key=#{school_permission_key}" %>
|
||||||
|
<td><%= link_to wrestler.name, wrestler_path_with_key %></td>
|
||||||
|
<% else %>
|
||||||
|
<td><%= link_to wrestler.name, wrestler_path(wrestler) %></td>
|
||||||
|
<% end %>
|
||||||
|
<td><%= wrestler.weight.max %></td>
|
||||||
|
<td><%= wrestler.season_win %>-<%= wrestler.season_loss %> <%= wrestler.criteria %></td>
|
||||||
|
<td><%= wrestler.original_seed %></td>
|
||||||
|
<td><%= wrestler.total_team_points - wrestler.total_points_deducted %></td>
|
||||||
|
<td><%= "Yes" if wrestler.extra? %></td>
|
||||||
|
<td><%= wrestler.next_match_bout_number %> <%= wrestler.next_match_mat_name %></td>
|
||||||
@@ -54,19 +54,8 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<% @wrestlers.sort_by { |w| w.weight.max }.each do |wrestler| %>
|
<% @wrestlers.sort_by { |w| w.weight.max }.each do |wrestler| %>
|
||||||
<% if params[:school_permission_key].present? %>
|
<% if params[:school_permission_key].present? %>
|
||||||
<!-- No caching when school_permission_key is present -->
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<%= render "schools/wrestler_row_cells", wrestler: wrestler, school_permission_key: params[:school_permission_key] %>
|
||||||
<% wrestler_path_with_key = wrestler_path(wrestler) %>
|
|
||||||
<% wrestler_path_with_key += "?school_permission_key=#{params[:school_permission_key]}" if params[:school_permission_key].present? %>
|
|
||||||
<%= link_to wrestler.name, wrestler_path_with_key %>
|
|
||||||
</td>
|
|
||||||
<td><%= wrestler.weight.max %></td>
|
|
||||||
<td><%= wrestler.season_win %>-<%= wrestler.season_loss %> <%= wrestler.criteria %></td>
|
|
||||||
<td><%= wrestler.original_seed %></td>
|
|
||||||
<td><%= wrestler.total_team_points - wrestler.total_points_deducted %></td>
|
|
||||||
<td><%= "Yes" if wrestler.extra? %></td>
|
|
||||||
<td><%= wrestler.next_match_bout_number %> <%= wrestler.next_match_mat_name %></td>
|
|
||||||
|
|
||||||
<% if can? :manage, wrestler.school %>
|
<% if can? :manage, wrestler.school %>
|
||||||
<td>
|
<td>
|
||||||
@@ -86,34 +75,21 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</tr>
|
</tr>
|
||||||
<% else %>
|
<% else %>
|
||||||
<!-- Use caching only when school_permission_key is NOT present -->
|
<tr>
|
||||||
<% cache ["#{wrestler.id}_school_show", @school] do %>
|
<% cache ["school_show_wrestler_cells", wrestler] do %>
|
||||||
<tr>
|
<%= render "schools/wrestler_row_cells", wrestler: wrestler %>
|
||||||
<td><%= link_to wrestler.name, wrestler_path(wrestler) %></td>
|
<% end %>
|
||||||
<td><%= wrestler.weight.max %></td>
|
<% if can? :manage, wrestler.school %>
|
||||||
<td><%= wrestler.season_win %>-<%= wrestler.season_loss %> <%= wrestler.criteria %></td>
|
<td>
|
||||||
<td><%= wrestler.original_seed %></td>
|
<%= link_to edit_wrestler_path(wrestler), class: "text-decoration-none" do %>
|
||||||
<td><%= wrestler.total_team_points - wrestler.total_points_deducted %></td>
|
<span class="fas fa-edit" aria-hidden="true"></span>
|
||||||
<td><%= "Yes" if wrestler.extra? %></td>
|
<% end %>
|
||||||
<td><%= wrestler.next_match_bout_number %> <%= wrestler.next_match_mat_name %></td>
|
<%= link_to wrestler_path(wrestler), data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete #{wrestler.name}? This will delete all of his matches." }, class: "text-decoration-none" do %>
|
||||||
<% end %>
|
<span class="fas fa-trash-alt" aria-hidden="true"></span>
|
||||||
<% if can? :manage, wrestler.school %>
|
<% end %>
|
||||||
<td>
|
</td>
|
||||||
<% edit_wrestler_path_with_key = edit_wrestler_path(wrestler) %>
|
<% end %>
|
||||||
<% edit_wrestler_path_with_key += "?school_permission_key=#{params[:school_permission_key]}" if params[:school_permission_key].present? %>
|
</tr>
|
||||||
|
|
||||||
<% delete_wrestler_path_with_key = wrestler_path(wrestler) %>
|
|
||||||
<% delete_wrestler_path_with_key += "?school_permission_key=#{params[:school_permission_key]}" if params[:school_permission_key].present? %>
|
|
||||||
|
|
||||||
<%= link_to edit_wrestler_path_with_key, class: "text-decoration-none" do %>
|
|
||||||
<span class="fas fa-edit" aria-hidden="true"></span>
|
|
||||||
<% end %>
|
|
||||||
<%= link_to delete_wrestler_path_with_key, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete #{wrestler.name}? This will delete all of his matches." }, class: "text-decoration-none" do %>
|
|
||||||
<span class="fas fa-trash-alt" aria-hidden="true"></span>
|
|
||||||
<% end %>
|
|
||||||
</td>
|
|
||||||
<% end %>
|
|
||||||
</tr>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
<li>Win by major: 1pt extra</li>
|
<li>Win by major: 1pt extra</li>
|
||||||
<li>Win by tech fall: 1.5pt extra</li>
|
<li>Win by tech fall: 1.5pt extra</li>
|
||||||
<li>Win by fall, default, dq: 2pt extra</li>
|
<li>Win by fall, default, dq: 2pt extra</li>
|
||||||
<li>BYE points: 2pt (if you win at least 1 match in a pool with a BYE)</li>
|
<li>BYE points: 2pt (if you win at least 1 match in a pool with a BYE). - This only applies if your pool has more BYEs than other pools in your bracket. This does not apply to weight classes with 1 pool.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>See placement points below (based on the largest bracket of the tournament)</p>
|
<p>See placement points below (based on the largest bracket of the tournament)</p>
|
||||||
<h4>Pool Types</h4>
|
<h4>Pool Types</h4>
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
<li>Win by major: 1pt extra</li>
|
<li>Win by major: 1pt extra</li>
|
||||||
<li>Win by tech: 1.5pt extra</li>
|
<li>Win by tech: 1.5pt extra</li>
|
||||||
<li>Win by fall, default, dq, etc: 2pt extra</li>
|
<li>Win by fall, default, dq, etc: 2pt extra</li>
|
||||||
<li>BYE points: 2pts if you have a bye in the championship bracket and win the next match. 1pt if you have a bye in the consolation bracket and win the next match.</li>
|
<li>BYE points: 2pts if you have a bye in the championship bracket and win the next match. 1pt if you have a bye in the consolation bracket and win the next match. - This only applies if you received a bye in a round with at least 1 match in your backet.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<br>
|
<br>
|
||||||
<h3>Modified 16 Man Double Elimination Information</h3>
|
<h3>Modified 16 Man Double Elimination Information</h3>
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
<br>
|
<br>
|
||||||
<h3>Future Plans</h3>
|
<h3>Future Plans</h3>
|
||||||
<br>
|
<br>
|
||||||
<p>Future development plans to support 32 and 64 man regulard double elimination, modified (5 per day match rule) 32 man double elimination, and true second double elimination brackets are underway.</p>
|
<p>Future development plans are underway to make the application more flexible, make changes after weigh ins easier, and to add functionality for a live scoreboard.</p>
|
||||||
<br>
|
<br>
|
||||||
<h3>Contact</h3>
|
<h3>Contact</h3>
|
||||||
<br>
|
<br>
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
<% if @tournaments.size > 0 %>
|
<% if @tournaments.size > 0 %>
|
||||||
<h3>My Tournaments</h3>
|
<h3>My Tournaments</h3>
|
||||||
<script>
|
|
||||||
// $(document).ready(function() {
|
|
||||||
// $('#tournamentList').dataTable();
|
|
||||||
// pagingType: "bootstrap";
|
|
||||||
// } );
|
|
||||||
</script>
|
|
||||||
<table class="table table-hover" id="tournamentList">
|
<table class="table table-hover" id="tournamentList">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
<% @final_match.each do |match| %>
|
<% @final_match.each do |match| %>
|
||||||
<div class="round">
|
<% cache ["bracket_final_match", match, match.wrestler1, match.wrestler2, @winner_place, params[:print].to_s] do %>
|
||||||
<div class="game">
|
<div class="round">
|
||||||
<div class="game-top "><%= match.w1_bracket_name.html_safe %> <span></span></div>
|
<div class="game">
|
||||||
<% if params[:print] %>
|
<div class="game-top "><%= match.w1_bracket_name.html_safe %> <span></span></div>
|
||||||
<div class="bout-number"><p><%= match.bout_number %> <%= match.bracket_score_string %></p><p><%= @winner_place %> Place Winner</p></div>
|
<% if params[:print] %>
|
||||||
<% else %>
|
<div class="bout-number"><p><%= match.bout_number %> <%= match.bracket_score_string %></p><p><%= @winner_place %> Place Winner</p></div>
|
||||||
<div class="bout-number"><p><%= link_to match.bout_number, spectate_match_path(match) %> <%= match.bracket_score_string %></p><p><%= @winner_place %> Place Winner</p></div>
|
<% else %>
|
||||||
<% end %>
|
<div class="bout-number"><p><%= link_to match.bout_number, spectate_match_path(match) %> <%= match.bracket_score_string %></p><p><%= @winner_place %> Place Winner</p></div>
|
||||||
<div class="game-bottom "><%= match.w2_bracket_name.html_safe %><span></span></div>
|
<% end %>
|
||||||
</div>
|
<div class="game-bottom "><%= match.w2_bracket_name.html_safe %><span></span></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
<style>
|
<style>
|
||||||
table.smallText tr td { font-size: 10px; }
|
table.smallText tr td { font-size: 10px; }
|
||||||
|
table.smallText {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
table.smallText th,
|
||||||
|
table.smallText td {
|
||||||
|
border: 1px solid #000;
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
* Bracket Layout Specifics
|
* Bracket Layout Specifics
|
||||||
*/
|
*/
|
||||||
.bracket {
|
.bracket {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 10px;
|
font-size: 10.5px;
|
||||||
|
gap: 2px;
|
||||||
}
|
}
|
||||||
.game {
|
.game {
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
min-height: 50px;
|
min-height: 58px;
|
||||||
/*background-color: #ddd;*/
|
/*background-color: #ddd;*/
|
||||||
border: 1px solid #000; /* Dark border so boxes stay visible when printed */
|
border: 1.5px solid #000; /* Dark border so boxes stay visible when printed */
|
||||||
margin: 5px;
|
margin: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*.game:after {
|
/*.game:after {
|
||||||
@@ -56,14 +64,15 @@ table.smallText tr td { font-size: 10px; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.game-top {
|
.game-top {
|
||||||
border-bottom:1px solid #000;
|
border-bottom:1.5px solid #000;
|
||||||
padding: 2px;
|
padding: 3px 4px;
|
||||||
min-height: 12px;
|
min-height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bout-number {
|
.bout-number {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
/*padding-top: 15px;*/
|
line-height: 1.35;
|
||||||
|
padding: 1px 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Style links within bout-number like default links */
|
/* Style links within bout-number like default links */
|
||||||
@@ -77,15 +86,29 @@ table.smallText tr td { font-size: 10px; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bracket-winner {
|
.bracket-winner {
|
||||||
border-bottom:1px solid #000;
|
border-bottom:1.5px solid #000;
|
||||||
padding: 2px;
|
padding: 3px 4px;
|
||||||
min-height: 12px;
|
min-height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-bottom {
|
.game-bottom {
|
||||||
border-top:1px solid #000;
|
border-top:1.5px solid #000;
|
||||||
padding: 2px;
|
padding: 3px 4px;
|
||||||
min-height: 12px;
|
min-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.game,
|
||||||
|
.game-top,
|
||||||
|
.game-bottom,
|
||||||
|
.bracket-winner,
|
||||||
|
table.smallText,
|
||||||
|
table.smallText th,
|
||||||
|
table.smallText td {
|
||||||
|
border-color: #000 !important;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<% if @tournament.tournament_type == "Pool to bracket" %>
|
<% if @tournament.tournament_type == "Pool to bracket" %>
|
||||||
@@ -95,8 +118,6 @@ table.smallText tr td { font-size: 10px; }
|
|||||||
<table class='smallText'>
|
<table class='smallText'>
|
||||||
<tr>
|
<tr>
|
||||||
<td valign="top" style="padding: 10px;">
|
<td valign="top" style="padding: 10px;">
|
||||||
<% @matches = @tournament.matches.select{|m| m.weight_id == @weight.id} %>
|
|
||||||
<% @wrestlers = Wrestler.where(weight_id: @weight.id) %>
|
|
||||||
<% @pools = @weight.pool_rounds(@matches) %>
|
<% @pools = @weight.pool_rounds(@matches) %>
|
||||||
<%= render 'pool' %>
|
<%= render 'pool' %>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
<div class="round">
|
<div class="round">
|
||||||
<% @round_matches.sort_by{|m| m.bracket_position_number}.each do |match| %>
|
<% @round_matches.sort_by{|m| m.bracket_position_number}.each do |match| %>
|
||||||
<div class="game">
|
<% cache ["bracket_round_match", match, match.wrestler1, match.wrestler2, params[:print].to_s] do %>
|
||||||
<div class="game-top "><%= match.w1_bracket_name.html_safe %> <span></span></div>
|
<div class="game">
|
||||||
<% if params[:print] %>
|
<div class="game-top "><%= match.w1_bracket_name.html_safe %> <span></span></div>
|
||||||
<div class="bout-number"><%= match.bout_number %> <%= match.bracket_score_string %> </div>
|
<% if params[:print] %>
|
||||||
<% else %>
|
<div class="bout-number"><%= match.bout_number %> <%= match.bracket_score_string %> </div>
|
||||||
<div class="bout-number"><%= link_to match.bout_number, spectate_match_path(match) %> <%= match.bracket_score_string %> </div>
|
<% else %>
|
||||||
<% end %>
|
<div class="bout-number"><%= link_to match.bout_number, spectate_match_path(match) %> <%= match.bracket_score_string %> </div>
|
||||||
<div class="bout-number">Round <%= match.round %></div>
|
<% end %>
|
||||||
<div class="bout-number"><%= match.bracket_position %></div>
|
<div class="bout-number">Round <%= match.round %></div>
|
||||||
<div class="game-bottom "><%= match.w2_bracket_name.html_safe %><span></span></div>
|
<div class="bout-number"><%= match.bracket_position %></div>
|
||||||
</div>
|
<div class="game-bottom "><%= match.w2_bracket_name.html_safe %><span></span></div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
103
app/views/tournaments/_live_score_card.html.erb
Normal file
103
app/views/tournaments/_live_score_card.html.erb
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<%
|
||||||
|
match = local_assigns[:match]
|
||||||
|
mat = local_assigns[:mat]
|
||||||
|
tournament = local_assigns[:tournament]
|
||||||
|
%>
|
||||||
|
|
||||||
|
<div
|
||||||
|
data-controller="match-scoreboard"
|
||||||
|
data-match-scoreboard-source-mode-value="mat_websocket"
|
||||||
|
data-match-scoreboard-display-mode-value="embedded"
|
||||||
|
data-match-scoreboard-match-id-value="<%= match&.id || 0 %>"
|
||||||
|
data-match-scoreboard-mat-id-value="<%= mat.id %>"
|
||||||
|
data-match-scoreboard-tournament-id-value="<%= tournament.id %>"
|
||||||
|
data-match-scoreboard-initial-bout-number-value="<%= match&.bout_number || 0 %>">
|
||||||
|
|
||||||
|
<div class="panel panel-default" style="margin-bottom: 0;">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Mat <%= mat.name %></strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table table-bordered table-condensed" style="margin-bottom: 0; table-layout: fixed;">
|
||||||
|
<tr class="active">
|
||||||
|
<td>
|
||||||
|
<strong data-match-scoreboard-target="boutLabel">Bout <%= match&.bout_number || "" %></strong>
|
||||||
|
<span style="margin-left: 12px;" data-match-scoreboard-target="weightLabel">Weight <%= match&.weight&.max || "-" %></span>
|
||||||
|
</td>
|
||||||
|
<td class="text-right" style="white-space: nowrap;">
|
||||||
|
<span class="label label-default" data-match-scoreboard-target="clock">-</span>
|
||||||
|
<span class="label label-primary" data-match-scoreboard-target="periodLabel">No Match</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="vertical-align: middle;">
|
||||||
|
<div data-match-scoreboard-target="greenName" style="font-weight: 700; font-size: 1.15em;">NO MATCH</div>
|
||||||
|
<div class="text-muted" data-match-scoreboard-target="greenSchool"></div>
|
||||||
|
<div data-match-scoreboard-target="greenTimerIndicator" style="margin-top: 6px;"></div>
|
||||||
|
</td>
|
||||||
|
<td data-match-scoreboard-target="greenSection" class="text-center" style="background: #1cab2d; color: #fff; font-size: 2rem; font-weight: 700; vertical-align: middle; width: 110px;">
|
||||||
|
<span data-match-scoreboard-target="greenScore">0</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="vertical-align: middle;">
|
||||||
|
<div data-match-scoreboard-target="redName" style="font-weight: 700; font-size: 1.15em;">NO MATCH</div>
|
||||||
|
<div class="text-muted" data-match-scoreboard-target="redSchool"></div>
|
||||||
|
<div data-match-scoreboard-target="redTimerIndicator" style="margin-top: 6px;"></div>
|
||||||
|
</td>
|
||||||
|
<td data-match-scoreboard-target="redSection" class="text-center" style="background: #c91f1f; color: #fff; font-size: 2rem; font-weight: 700; vertical-align: middle; width: 110px;">
|
||||||
|
<span data-match-scoreboard-target="redScore">0</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" style="padding: 0;">
|
||||||
|
<div class="panel panel-default" style="margin: 10px;">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<a data-toggle="collapse" href="#live-score-stats-<%= mat.id %>" aria-expanded="false" aria-controls="live-score-stats-<%= mat.id %>" style="display: flex; justify-content: space-between; align-items: center; color: #333; text-decoration: none; background: transparent; outline: none;">
|
||||||
|
<strong>Stats</strong>
|
||||||
|
<span class="text-muted" style="font-size: 0.9em;">Show/Hide</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div id="live-score-stats-<%= mat.id %>" class="panel-collapse collapse">
|
||||||
|
<div class="panel-body" style="padding-bottom: 0;">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="label label-success" style="display: inline-block; margin-bottom: 8px;">Green</div>
|
||||||
|
<pre data-match-scoreboard-target="greenStats" class="well well-sm" style="min-height: 100px; white-space: pre-wrap; overflow-wrap: anywhere; background: #fff;"></pre>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="label label-danger" style="display: inline-block; margin-bottom: 8px;">Red</div>
|
||||||
|
<pre data-match-scoreboard-target="redStats" class="well well-sm" style="min-height: 100px; white-space: pre-wrap; overflow-wrap: anywhere; background: #fff;"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" style="padding: 0;">
|
||||||
|
<div class="panel panel-default" style="margin: 10px;">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<a data-toggle="collapse" href="#live-score-last-result-<%= mat.id %>" aria-expanded="false" aria-controls="live-score-last-result-<%= mat.id %>" style="display: flex; justify-content: space-between; align-items: center; color: #333; text-decoration: none; background: transparent; outline: none;">
|
||||||
|
<strong>Last Match Result</strong>
|
||||||
|
<span class="text-muted" style="font-size: 0.9em;">Show/Hide</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div id="live-score-last-result-<%= mat.id %>" class="panel-collapse collapse">
|
||||||
|
<div class="panel-body">
|
||||||
|
<div data-match-scoreboard-target="lastMatchResult">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-match-scoreboard-target="emptyState" style="display: none;"></div>
|
||||||
|
<div data-match-scoreboard-target="centerSection" style="display: none;"></div>
|
||||||
|
<div data-match-scoreboard-target="timerBanner" style="display: none;"></div>
|
||||||
|
<div data-match-scoreboard-target="timerBannerLabel" style="display: none;"></div>
|
||||||
|
<div data-match-scoreboard-target="timerBannerClock" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
6
app/views/tournaments/_team_score_row.html.erb
Normal file
6
app/views/tournaments/_team_score_row.html.erb
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<% cache ["team_score_row", school, rank] do %>
|
||||||
|
<tr>
|
||||||
|
<td><%= rank %>. <%= school.name %> (<%= school.abbreviation %>)</td>
|
||||||
|
<td><%= school.page_score_string %></td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
38
app/views/tournaments/_up_matches_board.html.erb
Normal file
38
app/views/tournaments/_up_matches_board.html.erb
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<div id="up_matches_board">
|
||||||
|
<h3>Upcoming Matches</h3>
|
||||||
|
<table class="table table-striped table-bordered table-condensed">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Mat </th>
|
||||||
|
<th>On Mat</th>
|
||||||
|
<th>On Deck</th>
|
||||||
|
<th>In The Hole</th>
|
||||||
|
<th>Warm Up</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<% (local_assigns[:mats] || tournament.up_matches_mats).each do |m| %>
|
||||||
|
<%= render "tournaments/up_matches_mat_row", mat: m %>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<br>
|
||||||
|
<h3>Matches not assigned</h3>
|
||||||
|
<br>
|
||||||
|
<table class="table table-striped table-bordered table-condensed" id="matchList">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Round</th>
|
||||||
|
<th>Bout Number</th>
|
||||||
|
<th>Weight Class</th>
|
||||||
|
<th>Matchup</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<%= render partial: "tournaments/up_matches_unassigned_row", collection: (local_assigns[:matches] || tournament.up_matches_unassigned_matches), as: :match %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<% queue1_match, queue2_match, queue3_match, queue4_match = mat.queue_matches %>
|
<% queue1_match, queue2_match, queue3_match, queue4_match = mat.queue_matches %>
|
||||||
<% cache ["up_matches_mat_row", mat, mat.queue1, mat.queue2, mat.queue3, mat.queue4] do %>
|
<% queue_match_dependencies = [queue1_match, queue2_match, queue3_match, queue4_match].compact.flat_map { |match| [match, match.wrestler1, match.wrestler2] } %>
|
||||||
|
<% cache ["up_matches_mat_row", mat, *queue_match_dependencies] do %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= mat.name %></td>
|
<td><%= mat.name %></td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<% cache ["up_matches_unassigned_row", match, match.wrestler1, match.wrestler2] do %>
|
||||||
|
<tr>
|
||||||
|
<td>Round <%= match.round %></td>
|
||||||
|
<td><%= match.bout_number %></td>
|
||||||
|
<td><%= match.weight_max %></td>
|
||||||
|
<td><%= match.w1_bracket_name %> vs. <%= match.w2_bracket_name %></td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
<style>
|
<style>
|
||||||
/* General styles for pages */
|
/* General styles for pages */
|
||||||
@page {
|
@page {
|
||||||
margin: 0.5in; /* Universal margin for all pages */
|
margin: 0.35in;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
width: 7.5in; /* Portrait width (8.5in - margins) */
|
width: 7.8in; /* 8.5in - 2 * 0.35in */
|
||||||
height: 10in; /* Portrait height (11in - margins) */
|
height: 10.3in; /* 11in - 2 * 0.35in */
|
||||||
margin: auto;
|
margin: auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-landscape {
|
.page-landscape {
|
||||||
width: 10in; /* Landscape width (11in - margins) */
|
width: 10.3in; /* 11in - 2 * 0.35in */
|
||||||
height: 7.5in; /* Landscape height (8.5in - margins) */
|
height: 7.8in; /* 8.5in - 2 * 0.35in */
|
||||||
margin: auto;
|
margin: auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -26,6 +26,11 @@
|
|||||||
transform-origin: top left;
|
transform-origin: top left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bracket-container h4 {
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
margin-bottom: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Print-specific styles */
|
/* Print-specific styles */
|
||||||
@media print {
|
@media print {
|
||||||
/* Set orientation for portrait pages */
|
/* Set orientation for portrait pages */
|
||||||
@@ -51,6 +56,10 @@
|
|||||||
transform-origin: top left;
|
transform-origin: top left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bracket {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
/* Optional: Hide elements not needed in print */
|
/* Optional: Hide elements not needed in print */
|
||||||
.no-print {
|
.no-print {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -62,15 +71,10 @@
|
|||||||
function scaleContent() {
|
function scaleContent() {
|
||||||
document.querySelectorAll('.page, .page-landscape').forEach(page => {
|
document.querySelectorAll('.page, .page-landscape').forEach(page => {
|
||||||
const container = page.querySelector('.bracket-container');
|
const container = page.querySelector('.bracket-container');
|
||||||
const isLandscape = page.classList.contains('page-landscape');
|
|
||||||
|
|
||||||
// Page dimensions (1 inch = 96px)
|
// Use the actual page box size (already accounts for @page margins)
|
||||||
const pageWidth = isLandscape ? 10 * 96 : 7.5 * 96;
|
const availableWidth = page.clientWidth;
|
||||||
const pageHeight = isLandscape ? 7.5 * 96 : 10 * 96;
|
const availableHeight = page.clientHeight;
|
||||||
|
|
||||||
// Subtract margins (0.5 inch margin)
|
|
||||||
const availableWidth = pageWidth - (0.5 * 96 * 2);
|
|
||||||
const availableHeight = pageHeight - (0.5 * 96 * 2);
|
|
||||||
|
|
||||||
// Measure content dimensions
|
// Measure content dimensions
|
||||||
const contentWidth = container.scrollWidth;
|
const contentWidth = container.scrollWidth;
|
||||||
@@ -80,8 +84,8 @@
|
|||||||
const scaleX = availableWidth / contentWidth;
|
const scaleX = availableWidth / contentWidth;
|
||||||
const scaleY = availableHeight / contentHeight;
|
const scaleY = availableHeight / contentHeight;
|
||||||
|
|
||||||
// Use a slightly relaxed scaling to avoid over-aggressive shrinking
|
// Keep a tiny buffer so borders/text don't clip at print edges
|
||||||
const scale = Math.min(scaleX, scaleY, 1); // Ensure scale does not exceed 100% (1)
|
const scale = Math.min(scaleX, scaleY, 1) * 0.99;
|
||||||
|
|
||||||
// Apply the scale
|
// Apply the scale
|
||||||
container.style.transform = `scale(${scale})`;
|
container.style.transform = `scale(${scale})`;
|
||||||
@@ -91,9 +95,9 @@
|
|||||||
const scaledWidth = contentWidth * scale;
|
const scaledWidth = contentWidth * scale;
|
||||||
const scaledHeight = contentHeight * scale;
|
const scaledHeight = contentHeight * scale;
|
||||||
|
|
||||||
// Center the content within the page
|
// Center the content within the available page box
|
||||||
const horizontalPadding = (pageWidth - scaledWidth) / 2;
|
const horizontalPadding = (availableWidth - scaledWidth) / 2;
|
||||||
const verticalPadding = (pageHeight - scaledHeight) / 2;
|
const verticalPadding = (availableHeight - scaledHeight) / 2;
|
||||||
|
|
||||||
// Apply margin adjustments
|
// Apply margin adjustments
|
||||||
container.style.marginLeft = `${Math.max(0, horizontalPadding)}px`;
|
container.style.marginLeft = `${Math.max(0, horizontalPadding)}px`;
|
||||||
@@ -119,14 +123,15 @@
|
|||||||
<% @weights.sort_by{|w| w.max}.each do |weight| %>
|
<% @weights.sort_by{|w| w.max}.each do |weight| %>
|
||||||
<% if @tournament.tournament_type == "Pool to bracket" %>
|
<% if @tournament.tournament_type == "Pool to bracket" %>
|
||||||
<!-- Need to define what the tournaments#bracket controller defines -->
|
<!-- Need to define what the tournaments#bracket controller defines -->
|
||||||
<% @matches = @tournament.matches.select{|m| m.weight_id == weight.id} %>
|
<% @matches = @matches_by_weight_id[weight.id] || [] %>
|
||||||
<% @wrestlers = Wrestler.where(weight_id: weight.id) %>
|
<% @wrestlers = @wrestlers_by_weight_id[weight.id] || [] %>
|
||||||
<% @pools = weight.pool_rounds(@matches) %>
|
<% @pools = weight.pool_rounds(@matches) %>
|
||||||
<% @weight = weight %>
|
<% @weight = weight %>
|
||||||
<%= render 'bracket_partial' %>
|
<%= render 'bracket_partial' %>
|
||||||
<% elsif @tournament.tournament_type.include? "Modified 16 Man Double Elimination" or @tournament.tournament_type.include? "Regular Double Elimination" %>
|
<% elsif @tournament.tournament_type.include? "Modified 16 Man Double Elimination" or @tournament.tournament_type.include? "Regular Double Elimination" %>
|
||||||
<!-- Need to define what the tournaments#bracket controller defines -->
|
<!-- Need to define what the tournaments#bracket controller defines -->
|
||||||
<% @matches = weight.matches %>
|
<% @matches = @matches_by_weight_id[weight.id] || [] %>
|
||||||
|
<% @wrestlers = @wrestlers_by_weight_id[weight.id] || [] %>
|
||||||
<% @weight = weight %>
|
<% @weight = weight %>
|
||||||
<%= render 'bracket_partial' %>
|
<%= render 'bracket_partial' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -55,10 +55,10 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<% @matches.each do |match| %>
|
<% @matches.each do |match| %>
|
||||||
<% if match.w1 && match.w2 %>
|
<% w1 = @wrestlers_by_id[match.w1] %>
|
||||||
<% w1 = Wrestler.find(match.w1) %>
|
<% w2 = @wrestlers_by_id[match.w2] %>
|
||||||
<% w2 = Wrestler.find(match.w2) %>
|
<% w1_name = w1&.name || match.loser1_name %>
|
||||||
<% end %>
|
<% w2_name = w2&.name || match.loser2_name %>
|
||||||
|
|
||||||
<div class="pagebreak">
|
<div class="pagebreak">
|
||||||
<p><strong>Bout Number:</strong> <%= match.bout_number %> <strong>Weight Class:</strong> <%= match.weight.max %> <strong>Round:</strong> <%= match.round %> <strong>Bracket Position:</strong> <%= match.bracket_position %></p>
|
<p><strong>Bout Number:</strong> <%= match.bout_number %> <strong>Weight Class:</strong> <%= match.weight.max %> <strong>Round:</strong> <%= match.round %> <strong>Bracket Position:</strong> <%= match.bracket_position %></p>
|
||||||
@@ -69,10 +69,10 @@
|
|||||||
<tr class="small-row">
|
<tr class="small-row">
|
||||||
<th class="fixed-width">Circle Winner</th>
|
<th class="fixed-width">Circle Winner</th>
|
||||||
<th>
|
<th>
|
||||||
<p><%= match.w1_name %>-<%= w1&.school&.name %></p>
|
<p><%= w1_name %>-<%= w1&.school&.name %></p>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<p><%= match.w2_name %>-<%= w2&.school&.name %></p>
|
<p><%= w2_name %>-<%= w2&.school&.name %></p>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<% cache ["#{@weight.id}_bracket", @weight] do %>
|
<% cache ["#{@weight.id}_bracket", @weight, params[:print].to_s] do %>
|
||||||
<%= render 'bracket_partial' %>
|
<%= render 'bracket_partial' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if @tournament.tournament_type == "Pool to bracket" %>
|
<% if @tournament.tournament_type == "Pool to bracket" %>
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
<%
|
||||||
|
wrestlers_by_id = @tournament.wrestlers.index_by(&:id)
|
||||||
|
weights_by_id = @tournament.weights.index_by(&:id)
|
||||||
|
mats_by_id = @tournament.mats.index_by(&:id)
|
||||||
|
sorted_matches = @tournament.matches.sort_by(&:bout_number)
|
||||||
|
%>
|
||||||
{
|
{
|
||||||
"tournament": {
|
"tournament": {
|
||||||
"attributes": <%= @tournament.attributes.to_json %>,
|
"attributes": <%= @tournament.attributes.to_json %>,
|
||||||
@@ -14,13 +20,13 @@
|
|||||||
"weight": wrestler.weight&.attributes
|
"weight": wrestler.weight&.attributes
|
||||||
}
|
}
|
||||||
) }.to_json %>,
|
) }.to_json %>,
|
||||||
"matches": <%= @tournament.matches.sort_by(&:bout_number).map { |match| match.attributes.merge(
|
"matches": <%= sorted_matches.map { |match| match.attributes.merge(
|
||||||
{
|
{
|
||||||
"w1_name": Wrestler.find_by(id: match.w1)&.name,
|
"w1_name": wrestlers_by_id[match.w1]&.name,
|
||||||
"w2_name": Wrestler.find_by(id: match.w2)&.name,
|
"w2_name": wrestlers_by_id[match.w2]&.name,
|
||||||
"winner_name": Wrestler.find_by(id: match.winner_id)&.name,
|
"winner_name": wrestlers_by_id[match.winner_id]&.name,
|
||||||
"weight": Weight.find_by(id: match.weight_id)&.attributes,
|
"weight": weights_by_id[match.weight_id]&.attributes,
|
||||||
"mat": Mat.find_by(id: match.mat_id)&.attributes
|
"mat": mats_by_id[match.mat_id]&.attributes
|
||||||
}
|
}
|
||||||
) }.to_json %>
|
) }.to_json %>
|
||||||
}
|
}
|
||||||
|
|||||||
12
app/views/tournaments/live_scores.html.erb
Normal file
12
app/views/tournaments/live_scores.html.erb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<h1><%= @tournament.name %> Live Scores</h1>
|
||||||
|
|
||||||
|
<% if @mats.any? %>
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(520px, 1fr)); gap: 20px; align-items: start;">
|
||||||
|
<% @mats.each do |mat| %>
|
||||||
|
<% match = mat.selected_scoreboard_match || mat.queue1_match %>
|
||||||
|
<%= render "tournaments/live_score_card", mat: mat, match: match, tournament: @tournament %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<p>No mats have been created for this tournament.</p>
|
||||||
|
<% end %>
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
|
|
||||||
|
|
||||||
<h1>All <%= @tournament.name %> matches</h1>
|
<h1>All <%= @tournament.name %> matches</h1>
|
||||||
<script>
|
|
||||||
$(document).ready(function() {
|
<% matches_path = "/tournaments/#{@tournament.id}/matches" %>
|
||||||
$('#matchesList').dataTable();
|
|
||||||
pagingType: "bootstrap";
|
<%= form_tag(matches_path, method: :get, id: "search-form") do %>
|
||||||
} );
|
<%= text_field_tag :search, params[:search], placeholder: "Search wrestler, school, or bout #" %>
|
||||||
</script>
|
<%= submit_tag "Search" %>
|
||||||
</br>
|
<% end %>
|
||||||
|
|
||||||
|
<p>Search by wrestler name, school name, or bout number.</p>
|
||||||
|
|
||||||
|
<br>
|
||||||
<table class="table table-striped table-bordered table-condensed" id="matchesList">
|
<table class="table table-striped table-bordered table-condensed" id="matchesList">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -35,6 +37,49 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<% if @total_pages.present? && @total_pages > 1 %>
|
||||||
|
<nav aria-label="Matches pagination">
|
||||||
|
<ul class="pagination">
|
||||||
|
<% if @page > 1 %>
|
||||||
|
<li class="page-item">
|
||||||
|
<%= link_to "Previous", { controller: "tournaments", action: "matches", id: @tournament.id, page: @page - 1, search: params[:search] }, class: "page-link" %>
|
||||||
|
</li>
|
||||||
|
<% else %>
|
||||||
|
<li class="page-item disabled"><span class="page-link">Previous</span></li>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% window = 5
|
||||||
|
left = [1, @page - window / 2].max
|
||||||
|
right = [@total_pages, left + window - 1].min
|
||||||
|
left = [1, right - window + 1].max
|
||||||
|
%>
|
||||||
|
<% (left..right).each do |p| %>
|
||||||
|
<% if p == @page %>
|
||||||
|
<li class="page-item active"><span class="page-link"><%= p %></span></li>
|
||||||
|
<% else %>
|
||||||
|
<li class="page-item"><%= link_to p, { controller: "tournaments", action: "matches", id: @tournament.id, page: p, search: params[:search] }, class: "page-link" %></li>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if @page < @total_pages %>
|
||||||
|
<li class="page-item">
|
||||||
|
<%= link_to "Next", { controller: "tournaments", action: "matches", id: @tournament.id, page: @page + 1, search: params[:search] }, class: "page-link" %>
|
||||||
|
</li>
|
||||||
|
<% else %>
|
||||||
|
<li class="page-item disabled"><span class="page-link">Next</span></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<p class="text-muted">
|
||||||
|
<% start_index = ((@page - 1) * @per_page) + 1
|
||||||
|
end_index = [@page * @per_page, @total_count].min
|
||||||
|
%>
|
||||||
|
Showing <%= start_index %> - <%= end_index %> of <%= @total_count %> matches
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<p>Total matches without byes: <%= @matches.select{|m| m.loser1_name != 'BYE' and m.loser2_name != 'BYE'}.size %></p>
|
<p>Total matches without byes: <%= @matches_without_byes_count %></p>
|
||||||
<p>Unfinished matches: <%= @matches.select{|m| m.finished != 1 and m.loser1_name != 'BYE' and m.loser2_name != 'BYE'}.size %></p>
|
<p>Unfinished matches: <%= @unfinished_matches_without_byes_count %></p>
|
||||||
|
|||||||
@@ -129,13 +129,24 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
|
<th>Current Match</th>
|
||||||
<th><%= link_to " New Mat" , "/mats/new?tournament=#{@tournament.id}", :class=>"fas fa-plus" %></th>
|
<th><%= link_to " New Mat" , "/mats/new?tournament=#{@tournament.id}", :class=>"fas fa-plus" %></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% @mats.each do |mat| %>
|
<% @mats.each do |mat| %>
|
||||||
|
<% current_match = mat.queue1_match %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= link_to "Mat #{mat.name}", mat %></td>
|
<td><%= link_to "Mat #{mat.name}", mat %></td>
|
||||||
|
<td>
|
||||||
|
<% if current_match %>
|
||||||
|
<%= link_to "Stat Match", stat_match_path(current_match), class: "btn btn-primary btn-sm" %>
|
||||||
|
<%= link_to "State Match", state_mat_path(mat), class: "btn btn-success btn-sm" %>
|
||||||
|
<%= link_to "Scoreboard", scoreboard_mat_path(mat, print: true), class: "btn btn-warning btn-sm", target: "_blank", rel: "noopener" %>
|
||||||
|
<% else %>
|
||||||
|
<%= link_to "Scoreboard", scoreboard_mat_path(mat, print: true), class: "btn btn-warning btn-sm", target: "_blank", rel: "noopener" %>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
<% if can? :manage, @tournament %>
|
<% if can? :manage, @tournament %>
|
||||||
<td>
|
<td>
|
||||||
<%= link_to mat, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete Mat #{mat.name}?" }, class: "text-decoration-none" do %>
|
<%= link_to mat, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete Mat #{mat.name}?" }, class: "text-decoration-none" do %>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
|
<% team_scores_last_updated = @schools.map(&:updated_at).compact.max&.utc&.to_fs(:nsec) %>
|
||||||
<% cache ["#{@tournament.id}_team_scores", @tournament] do %>
|
<% cache ["team_scores", @tournament.id, @schools.size, team_scores_last_updated] do %>
|
||||||
|
|
||||||
<table class="pagebreak table table-striped table-bordered">
|
<table class="pagebreak table table-striped table-bordered">
|
||||||
<h3>Team Scores</h3>
|
<h3>Team Scores</h3>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -11,11 +10,8 @@
|
|||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<% @schools.each do |school| %>
|
<% @schools.each_with_index do |school, index| %>
|
||||||
<tr>
|
<%= render "tournaments/team_score_row", school: school, rank: index + 1 %>
|
||||||
<td><%= @schools.index(school) + 1 %>. <%= school.name %> (<%= school.abbreviation %>)</td>
|
|
||||||
<td><%= school.page_score_string %></td>
|
|
||||||
</tr>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,75 +1,19 @@
|
|||||||
<script>
|
<div data-controller="up-matches-connection">
|
||||||
// $(document).ready(function() {
|
<% if params[:print] != "true" %>
|
||||||
// $('#matchList').dataTable();
|
<div style="margin-bottom: 10px;">
|
||||||
// } );
|
<%= link_to "Show Bout Board in Full Screen", up_matches_path(@tournament, print: true), class: "btn btn-primary" %>
|
||||||
</script>
|
</div>
|
||||||
<script>
|
<% end %>
|
||||||
const setUpMatchesRefresh = () => {
|
|
||||||
if (window.__upMatchesRefreshTimeout) {
|
|
||||||
clearTimeout(window.__upMatchesRefreshTimeout);
|
|
||||||
}
|
|
||||||
window.__upMatchesRefreshTimeout = setTimeout(() => {
|
|
||||||
window.location.reload(true);
|
|
||||||
}, 30000);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("turbo:load", setUpMatchesRefresh);
|
<%= turbo_stream_from @tournament, data: { up_matches_connection_target: "stream" } %>
|
||||||
// turbo:before-cache stops the timer refresh from occurring if you navigate away from up_matches
|
<div
|
||||||
document.addEventListener("turbo:before-cache", () => {
|
id="up-matches-cable-status-indicator"
|
||||||
if (window.__upMatchesRefreshTimeout) {
|
data-up-matches-connection-target="statusIndicator"
|
||||||
clearTimeout(window.__upMatchesRefreshTimeout);
|
class="alert alert-secondary"
|
||||||
window.__upMatchesRefreshTimeout = null;
|
style="padding: 5px; margin-bottom: 10px; border-radius: 4px;"
|
||||||
}
|
>
|
||||||
});
|
Connecting to server for real-time up matches updates...
|
||||||
</script>
|
</div>
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<h5 style="color:red">This page reloads every 30s</h5>
|
|
||||||
<br>
|
|
||||||
<h3>Upcoming Matches</h3>
|
|
||||||
<br>
|
|
||||||
<table class="table table-striped table-bordered table-condensed">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Mat </th>
|
|
||||||
<th>On Mat</th>
|
|
||||||
<th>On Deck</th>
|
|
||||||
<th>In The Hole</th>
|
|
||||||
<th>Warm Up</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
<%= render "up_matches_board", tournament: @tournament, mats: @mats, matches: @matches %>
|
||||||
<% @mats.each.map do |m| %>
|
</div>
|
||||||
<%= render "up_matches_mat_row", mat: m %>
|
|
||||||
<% end %>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<br>
|
|
||||||
<h3>Matches not assigned</h3>
|
|
||||||
<br>
|
|
||||||
<table class="table table-striped table-bordered table-condensed" id="matchList">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Round</th>
|
|
||||||
<th>Bout Number</th>
|
|
||||||
<th>Weight Class</th>
|
|
||||||
<th>Matchup</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<% if @matches.size > 0 %>
|
|
||||||
<% @matches.each.map do |m| %>
|
|
||||||
<tr>
|
|
||||||
<td>Round <%= m.round %></td>
|
|
||||||
<td><%= m.bout_number %></td>
|
|
||||||
<td><%= m.weight_max %></td>
|
|
||||||
<td><%= m.w1_bracket_name %> vs. <%= m.w2_bracket_name %></td>
|
|
||||||
</tr>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|||||||
@@ -12,9 +12,10 @@
|
|||||||
height: 1in;
|
height: 1in;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<% @tournament.schools.each do |school| %>
|
<% @schools.each do |school| %>
|
||||||
<table class="table table-striped table-bordered table-condensed pagebreak">
|
<table class="table table-striped table-bordered table-condensed pagebreak">
|
||||||
<h5><%= school.name %></h4>
|
<h5><%= school.name %></h4>
|
||||||
|
<p><strong>Weigh In Ref:</strong> <%= @tournament.weigh_in_ref %></p>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><%= wrestler.name %></td>
|
<td><%= wrestler.name %></td>
|
||||||
<td><%= wrestler.weight.max %></td>
|
<td><%= wrestler.weight.max %></td>
|
||||||
<td></td>
|
<td><%= wrestler.offical_weight %></td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<%= form_tag @wrestlers_update_path do %>
|
<%= form_tag "/tournaments/#{@tournament.id}/weigh_in/#{@weight.id}", method: :post do %>
|
||||||
<% @wrestlers.order("original_seed asc").each do |wrestler| %>
|
<% @wrestlers.order("original_seed asc").each do |wrestler| %>
|
||||||
<% if wrestler.weight_id == @weight.id %>
|
<% if wrestler.weight_id == @weight.id %>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<td><%= wrestler.weight.max %></td>
|
<td><%= wrestler.weight.max %></td>
|
||||||
<td>
|
<td>
|
||||||
<% if user_signed_in? %>
|
<% if user_signed_in? %>
|
||||||
<%= fields_for "wrestler[]", wrestler do |w| %>
|
<%= fields_for "wrestler[#{wrestler.id}]", wrestler do |w| %>
|
||||||
<%= w.number_field :offical_weight, :step => 'any' %>
|
<%= w.number_field :offical_weight, :step => 'any' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|||||||
10
app/views/weights/_readonly_wrestler_row.html.erb
Normal file
10
app/views/weights/_readonly_wrestler_row.html.erb
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<% cache ["weight_show_wrestler_row", wrestler] do %>
|
||||||
|
<tr>
|
||||||
|
<td><%= link_to wrestler.name, wrestler %></td>
|
||||||
|
<td><%= wrestler.school.name %></td>
|
||||||
|
<td><%= wrestler.original_seed %></td>
|
||||||
|
<td><%= wrestler.season_win %>-<%= wrestler.season_loss %></td>
|
||||||
|
<td><%= wrestler.criteria %> Win <%= wrestler.season_win_percentage %>%</td>
|
||||||
|
<td><%= "Yes" if wrestler.extra? %></td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
@@ -14,35 +14,36 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% @wrestlers.sort_by{|w| [w.original_seed ? 0 : 1, w.original_seed || 0]}.each do |wrestler| %>
|
<% sorted_wrestlers = @wrestlers.sort_by{|w| [w.original_seed ? 0 : 1, w.original_seed || 0]} %>
|
||||||
<% if wrestler.weight_id == @weight.id %>
|
<% if can? :manage, @tournament %>
|
||||||
<tr>
|
<% sorted_wrestlers.each do |wrestler| %>
|
||||||
<td><%= link_to "#{wrestler.name}", wrestler %></td>
|
<% if wrestler.weight_id == @weight.id %>
|
||||||
<td><%= wrestler.school.name %></td>
|
<tr>
|
||||||
<td>
|
<td><%= link_to wrestler.name, wrestler %></td>
|
||||||
<% if can? :manage, @tournament %>
|
<td><%= wrestler.school.name %></td>
|
||||||
<%= fields_for "wrestler[]", wrestler do |w| %>
|
<td>
|
||||||
<%= w.text_field :original_seed %>
|
<%= fields_for "wrestler[]", wrestler do |w| %>
|
||||||
<% end %>
|
<%= w.text_field :original_seed %>
|
||||||
<% else %>
|
<% end %>
|
||||||
<%= wrestler.original_seed %>
|
</td>
|
||||||
<% end %>
|
<td><%= wrestler.season_win %>-<%= wrestler.season_loss %></td>
|
||||||
</td>
|
<td><%= wrestler.criteria %> Win <%= wrestler.season_win_percentage %>%</td>
|
||||||
<td><%= wrestler.season_win %>-<%= wrestler.season_loss %></td>
|
<td><%= "Yes" if wrestler.extra? %></td>
|
||||||
<td><%= wrestler.criteria %> Win <%= wrestler.season_win_percentage %>%</td>
|
<td>
|
||||||
<td><% if wrestler.extra? == true %>
|
<%= link_to wrestler, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete #{wrestler.name}? THIS WILL DELETE ALL MATCHES." }, class: "text-decoration-none" do %>
|
||||||
Yes
|
<span class="fas fa-trash-alt" aria-hidden="true"></span>
|
||||||
<% end %></td>
|
<% end %>
|
||||||
<% if can? :manage, @tournament %>
|
</td>
|
||||||
<td>
|
</tr>
|
||||||
<%= link_to wrestler, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete #{wrestler.name}? THIS WILL DELETE ALL MATCHES." }, class: "text-decoration-none" do %>
|
<% end %>
|
||||||
<span class="fas fa-trash-alt" aria-hidden="true"></span>
|
<% end %>
|
||||||
<% end %>
|
<% else %>
|
||||||
</td>
|
<% sorted_wrestlers.each do |wrestler| %>
|
||||||
<% end %>
|
<% if wrestler.weight_id == @weight.id %>
|
||||||
</tr>
|
<%= render "weights/readonly_wrestler_row", wrestler: wrestler %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% end %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<br><p>*All wrestlers without a seed (determined by tournament director) will be assigned a random bracket line.</p>
|
<br><p>*All wrestlers without a seed (determined by tournament director) will be assigned a random bracket line.</p>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
project_dir="$(dirname $( dirname $(readlink -f ${BASH_SOURCE[0]})))"
|
project_dir="$(dirname $( dirname $(readlink -f ${BASH_SOURCE[0]})))"
|
||||||
|
|
||||||
cd ${project_dir}
|
cd ${project_dir}
|
||||||
|
npm install
|
||||||
|
npm run test:js
|
||||||
bundle exec rake db:migrate RAILS_ENV=test
|
bundle exec rake db:migrate RAILS_ENV=test
|
||||||
CI=true brakeman
|
CI=true brakeman
|
||||||
bundle audit
|
bundle audit
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ project_dir="$(dirname $(readlink -f ${BASH_SOURCE[0]}))/.."
|
|||||||
|
|
||||||
docker build -f ${project_dir}/deploy/rails-prod-Dockerfile -t wrestlingdevtests ${project_dir}/.
|
docker build -f ${project_dir}/deploy/rails-prod-Dockerfile -t wrestlingdevtests ${project_dir}/.
|
||||||
docker run --rm -it wrestlingdevtests bash /rails/bin/run-all-tests.sh
|
docker run --rm -it wrestlingdevtests bash /rails/bin/run-all-tests.sh
|
||||||
bash ${project_dir}/cypress-tests/run-cypress-tests.sh
|
# bash ${project_dir}/cypress-tests/run-cypress-tests.sh
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
# Async adapter only works within the same process, so for manually triggering cable updates from a console,
|
|
||||||
# and seeing results in the browser, you must do so from the web console (running inside the dev process),
|
|
||||||
# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view
|
|
||||||
# to make the web console appear.
|
|
||||||
development:
|
development:
|
||||||
adapter: async
|
adapter: solid_cable
|
||||||
|
connects_to:
|
||||||
|
database:
|
||||||
|
writing: cable
|
||||||
|
polling_interval: 0.1.seconds
|
||||||
|
message_retention: 1.day
|
||||||
|
|
||||||
test:
|
test:
|
||||||
adapter: test
|
adapter: test
|
||||||
|
|||||||
@@ -11,15 +11,21 @@ pin "@rails/actioncable", to: "actioncable.esm.js" # For Action Cable
|
|||||||
# and pin it directly, e.g., pin "jquery", to: "jquery.min.js"
|
# and pin it directly, e.g., pin "jquery", to: "jquery.min.js"
|
||||||
pin "jquery", to: "jquery.js"
|
pin "jquery", to: "jquery.js"
|
||||||
|
|
||||||
# Pin Bootstrap and DataTables from vendor/assets/javascripts/
|
# Pin Bootstrap from vendor/assets/javascripts/
|
||||||
pin "bootstrap", to: "bootstrap.min.js"
|
pin "bootstrap", to: "bootstrap.min.js"
|
||||||
pin "datatables.net", to: "jquery.dataTables.min.js" # Assuming this is how you want to import it
|
|
||||||
|
|
||||||
# If Bootstrap requires Popper.js, and you have it in vendor/assets/javascripts/
|
# If Bootstrap requires Popper.js, and you have it in vendor/assets/javascripts/
|
||||||
# pin "@popperjs/core", to: "popper.min.js" # Or the actual filename if different
|
# pin "@popperjs/core", to: "popper.min.js" # Or the actual filename if different
|
||||||
|
|
||||||
# Pin controllers from app/assets/javascripts/controllers
|
# Pin controllers from app/assets/javascripts/controllers
|
||||||
pin_all_from "app/assets/javascripts/controllers", under: "controllers"
|
pin_all_from "app/assets/javascripts/controllers", under: "controllers"
|
||||||
|
pin "match-state-config", to: "lib/match_state/config.js"
|
||||||
|
pin "match-state-engine", to: "lib/match_state/engine.js"
|
||||||
|
pin "match-state-serializers", to: "lib/match_state/serializers.js"
|
||||||
|
pin "match-state-presenters", to: "lib/match_state/presenters.js"
|
||||||
|
pin "match-state-transport", to: "lib/match_state/transport.js"
|
||||||
|
pin "match-state-scoreboard-presenters", to: "lib/match_state/scoreboard_presenters.js"
|
||||||
|
pin "match-state-scoreboard-state", to: "lib/match_state/scoreboard_state.js"
|
||||||
|
|
||||||
# Pin all JS files from app/assets/javascripts directory
|
# Pin all JS files from app/assets/javascripts directory
|
||||||
pin_all_from "app/assets/javascripts", under: "assets/javascripts"
|
pin_all_from "app/assets/javascripts", under: "assets/javascripts"
|
||||||
@@ -3,12 +3,19 @@ Wrestling::Application.routes.draw do
|
|||||||
mount ActionCable.server => '/cable'
|
mount ActionCable.server => '/cable'
|
||||||
mount MissionControl::Jobs::Engine, at: "/jobs"
|
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||||
|
|
||||||
resources :mats
|
resources :mats do
|
||||||
|
member do
|
||||||
|
get :state
|
||||||
|
get :scoreboard
|
||||||
|
post :select_match
|
||||||
|
end
|
||||||
|
end
|
||||||
post "mats/:id/assign_next_match" => "mats#assign_next_match", :as => :assign_next_match
|
post "mats/:id/assign_next_match" => "mats#assign_next_match", :as => :assign_next_match
|
||||||
|
|
||||||
resources :matches do
|
resources :matches do
|
||||||
member do
|
member do
|
||||||
get :stat
|
get :stat
|
||||||
|
get :state
|
||||||
get :spectate
|
get :spectate
|
||||||
get :edit_assignment
|
get :edit_assignment
|
||||||
patch :update_assignment
|
patch :update_assignment
|
||||||
@@ -74,6 +81,7 @@ Wrestling::Application.routes.draw do
|
|||||||
get 'tournaments/:id/no_matches' => 'tournaments#no_matches'
|
get 'tournaments/:id/no_matches' => 'tournaments#no_matches'
|
||||||
get 'tournaments/:id/matches' => 'tournaments#matches'
|
get 'tournaments/:id/matches' => 'tournaments#matches'
|
||||||
get 'tournaments/:id/qrcode' => 'tournaments#qrcode'
|
get 'tournaments/:id/qrcode' => 'tournaments#qrcode'
|
||||||
|
get 'tournaments/:id/live_scores' => 'tournaments#live_scores'
|
||||||
get 'tournaments/:id/delegate' => 'tournaments#delegate', :as => :tournament_delegate
|
get 'tournaments/:id/delegate' => 'tournaments#delegate', :as => :tournament_delegate
|
||||||
post 'tournaments/:id/delegate' => 'tournaments#delegate', :as => :set_tournament_delegate
|
post 'tournaments/:id/delegate' => 'tournaments#delegate', :as => :set_tournament_delegate
|
||||||
delete 'tournaments/:id/:delegate/remove_delegate' => 'tournaments#remove_delegate', :as => :delete_delegate_path
|
delete 'tournaments/:id/:delegate/remove_delegate' => 'tournaments#remove_delegate', :as => :delete_delegate_path
|
||||||
|
|||||||
2162
package-lock.json
generated
Normal file
2162
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
package.json
Normal file
12
package.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "wrestlingapp-js-tests",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test:js": "vitest run",
|
||||||
|
"test:js:watch": "vitest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitest": "^3.2.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
110
test/channels/mat_scoreboard_channel_test.rb
Normal file
110
test/channels/mat_scoreboard_channel_test.rb
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class MatScoreboardChannelTest < ActionCable::Channel::TestCase
|
||||||
|
setup do
|
||||||
|
@mat = mats(:one)
|
||||||
|
@match = matches(:tournament_1_bout_1000)
|
||||||
|
@alternate_match = matches(:tournament_1_bout_1001)
|
||||||
|
Rails.cache.clear
|
||||||
|
@mat.update!(queue1: @match.id, queue2: @alternate_match.id, queue3: nil, queue4: nil)
|
||||||
|
@mat.set_selected_scoreboard_match!(@match)
|
||||||
|
@mat.set_last_match_result!("106 lbs - Example Winner Decision Example Loser 3-1")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "subscribes to a valid mat stream and transmits scoreboard payload" do
|
||||||
|
subscribe(mat_id: @mat.id)
|
||||||
|
|
||||||
|
assert subscription.confirmed?
|
||||||
|
assert_has_stream_for @mat
|
||||||
|
assert_equal(
|
||||||
|
{
|
||||||
|
"mat_id" => @mat.id,
|
||||||
|
"queue1_bout_number" => @match.bout_number,
|
||||||
|
"queue1_match_id" => @match.id,
|
||||||
|
"selected_bout_number" => @match.bout_number,
|
||||||
|
"selected_match_id" => @match.id,
|
||||||
|
"last_match_result" => "106 lbs - Example Winner Decision Example Loser 3-1"
|
||||||
|
},
|
||||||
|
transmissions.last
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rejects subscription for an invalid mat" do
|
||||||
|
subscribe(mat_id: -1)
|
||||||
|
|
||||||
|
assert subscription.rejected?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "transmits payload with queue1 and no selected match" do
|
||||||
|
@mat.set_selected_scoreboard_match!(nil)
|
||||||
|
|
||||||
|
subscribe(mat_id: @mat.id)
|
||||||
|
|
||||||
|
assert_equal(
|
||||||
|
{
|
||||||
|
"mat_id" => @mat.id,
|
||||||
|
"queue1_bout_number" => @match.bout_number,
|
||||||
|
"queue1_match_id" => @match.id,
|
||||||
|
"selected_bout_number" => nil,
|
||||||
|
"selected_match_id" => nil,
|
||||||
|
"last_match_result" => "106 lbs - Example Winner Decision Example Loser 3-1"
|
||||||
|
},
|
||||||
|
transmissions.last
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "transmits payload when selected match differs from queue1" do
|
||||||
|
@mat.set_selected_scoreboard_match!(@alternate_match)
|
||||||
|
|
||||||
|
subscribe(mat_id: @mat.id)
|
||||||
|
|
||||||
|
assert_equal(
|
||||||
|
{
|
||||||
|
"mat_id" => @mat.id,
|
||||||
|
"queue1_bout_number" => @match.bout_number,
|
||||||
|
"queue1_match_id" => @match.id,
|
||||||
|
"selected_bout_number" => @alternate_match.bout_number,
|
||||||
|
"selected_match_id" => @alternate_match.id,
|
||||||
|
"last_match_result" => "106 lbs - Example Winner Decision Example Loser 3-1"
|
||||||
|
},
|
||||||
|
transmissions.last
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "transmits payload when no queue1 match exists" do
|
||||||
|
@mat.update!(queue1: nil, queue2: nil, queue3: nil, queue4: nil)
|
||||||
|
@mat.set_selected_scoreboard_match!(nil)
|
||||||
|
|
||||||
|
subscribe(mat_id: @mat.id)
|
||||||
|
|
||||||
|
assert_equal(
|
||||||
|
{
|
||||||
|
"mat_id" => @mat.id,
|
||||||
|
"queue1_bout_number" => nil,
|
||||||
|
"queue1_match_id" => nil,
|
||||||
|
"selected_bout_number" => nil,
|
||||||
|
"selected_match_id" => nil,
|
||||||
|
"last_match_result" => "106 lbs - Example Winner Decision Example Loser 3-1"
|
||||||
|
},
|
||||||
|
transmissions.last
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "transmits payload with blank last match result" do
|
||||||
|
@mat.set_last_match_result!(nil)
|
||||||
|
|
||||||
|
subscribe(mat_id: @mat.id)
|
||||||
|
|
||||||
|
assert_equal(
|
||||||
|
{
|
||||||
|
"mat_id" => @mat.id,
|
||||||
|
"queue1_bout_number" => @match.bout_number,
|
||||||
|
"queue1_match_id" => @match.id,
|
||||||
|
"selected_bout_number" => @match.bout_number,
|
||||||
|
"selected_match_id" => @match.id,
|
||||||
|
"last_match_result" => nil
|
||||||
|
},
|
||||||
|
transmissions.last
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,8 +1,152 @@
|
|||||||
require "test_helper"
|
require "test_helper"
|
||||||
|
|
||||||
class MatchChannelTest < ActionCable::Channel::TestCase
|
class MatchChannelTest < ActionCable::Channel::TestCase
|
||||||
# test "subscribes" do
|
setup do
|
||||||
# subscribe
|
@match = matches(:tournament_1_bout_1000)
|
||||||
# assert subscription.confirmed?
|
Rails.cache.clear
|
||||||
# end
|
end
|
||||||
|
|
||||||
|
test "subscribes to a valid match stream" do
|
||||||
|
subscribe(match_id: @match.id)
|
||||||
|
|
||||||
|
assert subscription.confirmed?
|
||||||
|
assert_has_stream_for @match
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invalid match subscription confirms but does not stream" do
|
||||||
|
subscribe(match_id: -1)
|
||||||
|
|
||||||
|
assert subscription.confirmed?
|
||||||
|
assert_empty subscription.streams
|
||||||
|
end
|
||||||
|
|
||||||
|
test "send_stat updates the match and broadcasts stats" do
|
||||||
|
subscribe(match_id: @match.id)
|
||||||
|
|
||||||
|
assert_broadcast_on(@match, { w1_stat: "T3", w2_stat: "E1" }) do
|
||||||
|
perform :send_stat, {
|
||||||
|
new_w1_stat: "T3",
|
||||||
|
new_w2_stat: "E1"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@match.reload
|
||||||
|
assert_equal "T3", @match.w1_stat
|
||||||
|
assert_equal "E1", @match.w2_stat
|
||||||
|
end
|
||||||
|
|
||||||
|
test "send_stat updates only w1 stat when only w1 is provided" do
|
||||||
|
subscribe(match_id: @match.id)
|
||||||
|
|
||||||
|
assert_broadcast_on(@match, { w1_stat: "T3", w2_stat: nil }.compact) do
|
||||||
|
perform :send_stat, { new_w1_stat: "T3" }
|
||||||
|
end
|
||||||
|
|
||||||
|
@match.reload
|
||||||
|
assert_equal "T3", @match.w1_stat
|
||||||
|
assert_nil @match.w2_stat
|
||||||
|
end
|
||||||
|
|
||||||
|
test "send_stat updates only w2 stat when only w2 is provided" do
|
||||||
|
subscribe(match_id: @match.id)
|
||||||
|
|
||||||
|
assert_broadcast_on(@match, { w1_stat: nil, w2_stat: "E1" }.compact) do
|
||||||
|
perform :send_stat, { new_w2_stat: "E1" }
|
||||||
|
end
|
||||||
|
|
||||||
|
@match.reload
|
||||||
|
assert_nil @match.w1_stat
|
||||||
|
assert_equal "E1", @match.w2_stat
|
||||||
|
end
|
||||||
|
|
||||||
|
test "send_stat with empty payload does not update or broadcast" do
|
||||||
|
subscribe(match_id: @match.id)
|
||||||
|
stream = MatchChannel.broadcasting_for(@match)
|
||||||
|
ActionCable.server.pubsub.broadcasts(stream).clear
|
||||||
|
|
||||||
|
perform :send_stat, {}
|
||||||
|
|
||||||
|
@match.reload
|
||||||
|
assert_nil @match.w1_stat
|
||||||
|
assert_nil @match.w2_stat
|
||||||
|
assert_empty ActionCable.server.pubsub.broadcasts(stream)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "send_scoreboard caches and broadcasts scoreboard state" do
|
||||||
|
subscribe(match_id: @match.id)
|
||||||
|
scoreboard_state = {
|
||||||
|
"participantScores" => { "w1" => 2, "w2" => 0 },
|
||||||
|
"metadata" => { "boutNumber" => @match.bout_number }
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_broadcast_on(@match, { scoreboard_state: scoreboard_state }) do
|
||||||
|
perform :send_scoreboard, { scoreboard_state: scoreboard_state }
|
||||||
|
end
|
||||||
|
|
||||||
|
cached_state = Rails.cache.read("tournament:#{@match.tournament_id}:match:#{@match.id}:scoreboard_state")
|
||||||
|
assert_equal scoreboard_state, cached_state
|
||||||
|
end
|
||||||
|
|
||||||
|
test "send_scoreboard with blank payload does not cache or broadcast" do
|
||||||
|
subscribe(match_id: @match.id)
|
||||||
|
stream = MatchChannel.broadcasting_for(@match)
|
||||||
|
ActionCable.server.pubsub.broadcasts(stream).clear
|
||||||
|
|
||||||
|
perform :send_scoreboard, { scoreboard_state: nil }
|
||||||
|
|
||||||
|
assert_nil Rails.cache.read("tournament:#{@match.tournament_id}:match:#{@match.id}:scoreboard_state")
|
||||||
|
assert_empty ActionCable.server.pubsub.broadcasts(stream)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "request_sync transmits match data and cached scoreboard state" do
|
||||||
|
@match.update!(
|
||||||
|
w1_stat: "T3",
|
||||||
|
w2_stat: "E1",
|
||||||
|
score: "3-1",
|
||||||
|
win_type: "Decision",
|
||||||
|
winner_id: @match.w1,
|
||||||
|
finished: 1
|
||||||
|
)
|
||||||
|
scoreboard_state = {
|
||||||
|
"participantScores" => { "w1" => 3, "w2" => 1 },
|
||||||
|
"metadata" => { "boutNumber" => @match.bout_number }
|
||||||
|
}
|
||||||
|
Rails.cache.write(
|
||||||
|
"tournament:#{@match.tournament_id}:match:#{@match.id}:scoreboard_state",
|
||||||
|
scoreboard_state
|
||||||
|
)
|
||||||
|
|
||||||
|
subscribe(match_id: @match.id)
|
||||||
|
perform :request_sync
|
||||||
|
|
||||||
|
assert_equal({
|
||||||
|
"w1_stat" => "T3",
|
||||||
|
"w2_stat" => "E1",
|
||||||
|
"score" => "3-1",
|
||||||
|
"win_type" => "Decision",
|
||||||
|
"winner_name" => @match.wrestler1.name,
|
||||||
|
"winner_id" => @match.w1,
|
||||||
|
"finished" => 1,
|
||||||
|
"scoreboard_state" => scoreboard_state
|
||||||
|
}, transmissions.last)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "request_sync transmits unfinished match data without scoreboard cache" do
|
||||||
|
@match.update!(
|
||||||
|
w1_stat: "T3",
|
||||||
|
w2_stat: "E1",
|
||||||
|
score: nil,
|
||||||
|
win_type: nil,
|
||||||
|
winner_id: nil,
|
||||||
|
finished: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
subscribe(match_id: @match.id)
|
||||||
|
perform :request_sync
|
||||||
|
|
||||||
|
assert_equal({
|
||||||
|
"w1_stat" => "T3",
|
||||||
|
"w2_stat" => "E1"
|
||||||
|
}, transmissions.last)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -216,4 +216,20 @@ class MatAssignmentRulesControllerTest < ActionController::TestCase
|
|||||||
assert_equal ['A1', 'B2'], rule.bracket_positions
|
assert_equal ['A1', 'B2'], rule.bracket_positions
|
||||||
assert_equal [1, 2], rule.rounds
|
assert_equal [1, 2], rule.rounds
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "index lists created mat assignment rule once in html" do
|
||||||
|
sign_in_owner
|
||||||
|
unique_mat = Mat.create!(name: "Unique Mat #{SecureRandom.hex(4)}", tournament_id: @tournament.id)
|
||||||
|
MatAssignmentRule.create!(
|
||||||
|
mat_id: unique_mat.id,
|
||||||
|
tournament_id: @tournament.id,
|
||||||
|
weight_classes: [1],
|
||||||
|
bracket_positions: ['1/2'],
|
||||||
|
rounds: [2]
|
||||||
|
)
|
||||||
|
|
||||||
|
index
|
||||||
|
assert_response :success
|
||||||
|
assert_equal 1, response.body.scan(unique_mat.name).size
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
require 'test_helper'
|
require 'test_helper'
|
||||||
|
require "json"
|
||||||
|
|
||||||
class MatchesControllerTest < ActionController::TestCase
|
class MatchesControllerTest < ActionController::TestCase
|
||||||
# Remove Devise helpers since we're no longer using Devise
|
# Remove Devise helpers since we're no longer using Devise
|
||||||
# include Devise::Test::ControllerHelpers # Needed to sign in
|
# include Devise::Test::ControllerHelpers # Needed to sign in
|
||||||
include ActionView::Helpers::DateHelper # Needed for time ago in words
|
include ActionView::Helpers::DateHelper # Needed for time ago in words
|
||||||
|
include ActionCable::TestHelper
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
@tournament = Tournament.find(1)
|
@tournament = Tournament.find(1)
|
||||||
@@ -34,6 +36,18 @@ class MatchesControllerTest < ActionController::TestCase
|
|||||||
get :stat, params: { id: @match.id }
|
get :stat, params: { id: @match.id }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_state
|
||||||
|
get :state, params: { id: @match.id }
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_state_with_params(extra_params = {})
|
||||||
|
get :state, params: { id: @match.id }.merge(extra_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_spectate(extra_params = {})
|
||||||
|
get :spectate, params: { id: @match.id }.merge(extra_params)
|
||||||
|
end
|
||||||
|
|
||||||
def get_edit_assignment(extra_params = {})
|
def get_edit_assignment(extra_params = {})
|
||||||
get :edit_assignment, params: { id: @match.id }.merge(extra_params)
|
get :edit_assignment, params: { id: @match.id }.merge(extra_params)
|
||||||
end
|
end
|
||||||
@@ -106,11 +120,44 @@ class MatchesControllerTest < ActionController::TestCase
|
|||||||
redirect
|
redirect
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "logged in user should not get state match page if not owner" do
|
||||||
|
sign_in_non_owner
|
||||||
|
get_state
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged school delegate should not get state match page if not owner" do
|
||||||
|
sign_in_school_delegate
|
||||||
|
get_state
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
test "non logged in user should not get stat match page" do
|
test "non logged in user should not get stat match page" do
|
||||||
get_stat
|
get_stat
|
||||||
redirect
|
redirect
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "non logged in user should not get state match page" do
|
||||||
|
get_state
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
|
test "valid school permission key cannot get state match page" do
|
||||||
|
school = @tournament.schools.first
|
||||||
|
school.update!(permission_key: "valid-school-key")
|
||||||
|
|
||||||
|
get_state_with_params(school_permission_key: "valid-school-key")
|
||||||
|
assert_redirected_to "/static_pages/not_allowed"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invalid school permission key cannot get state match page" do
|
||||||
|
school = @tournament.schools.first
|
||||||
|
school.update!(permission_key: "valid-school-key")
|
||||||
|
|
||||||
|
get_state_with_params(school_permission_key: "invalid-school-key")
|
||||||
|
assert_redirected_to "/static_pages/not_allowed"
|
||||||
|
end
|
||||||
|
|
||||||
test "non logged in user should get post update match" do
|
test "non logged in user should get post update match" do
|
||||||
post_update
|
post_update
|
||||||
assert_redirected_to '/static_pages/not_allowed'
|
assert_redirected_to '/static_pages/not_allowed'
|
||||||
@@ -140,6 +187,202 @@ class MatchesControllerTest < ActionController::TestCase
|
|||||||
success
|
success
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "logged in tournament owner should get state match page" do
|
||||||
|
sign_in_owner
|
||||||
|
get_state
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in tournament delegate should get state match page" do
|
||||||
|
sign_in_tournament_delegate
|
||||||
|
get_state
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in school delegate cannot get spectate match page when tournament is not public" do
|
||||||
|
@tournament.update!(is_public: false)
|
||||||
|
sign_in_school_delegate
|
||||||
|
get_spectate
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in user cannot get spectate match page when tournament is not public" do
|
||||||
|
@tournament.update!(is_public: false)
|
||||||
|
sign_in_non_owner
|
||||||
|
get_spectate
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in tournament delegate can get spectate match page when tournament is not public" do
|
||||||
|
@tournament.update!(is_public: false)
|
||||||
|
sign_in_tournament_delegate
|
||||||
|
get_spectate
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in tournament owner can get spectate match page when tournament is not public" do
|
||||||
|
@tournament.update!(is_public: false)
|
||||||
|
sign_in_owner
|
||||||
|
get_spectate
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non logged in user cannot get spectate match page when tournament is not public" do
|
||||||
|
@tournament.update!(is_public: false)
|
||||||
|
get_spectate
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
|
test "valid school permission key cannot get spectate match page when tournament is not public" do
|
||||||
|
@tournament.update!(is_public: false)
|
||||||
|
school = @tournament.schools.first
|
||||||
|
school.update!(permission_key: "valid-school-key")
|
||||||
|
|
||||||
|
get_spectate(school_permission_key: "valid-school-key")
|
||||||
|
assert_redirected_to "/static_pages/not_allowed"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invalid school permission key cannot get spectate match page when tournament is not public" do
|
||||||
|
@tournament.update!(is_public: false)
|
||||||
|
school = @tournament.schools.first
|
||||||
|
school.update!(permission_key: "valid-school-key")
|
||||||
|
|
||||||
|
get_spectate(school_permission_key: "invalid-school-key")
|
||||||
|
assert_redirected_to "/static_pages/not_allowed"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in school delegate can get spectate match page when tournament is public" do
|
||||||
|
@tournament.update!(is_public: true)
|
||||||
|
sign_in_school_delegate
|
||||||
|
get_spectate
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in user can get spectate match page when tournament is public" do
|
||||||
|
@tournament.update!(is_public: true)
|
||||||
|
sign_in_non_owner
|
||||||
|
get_spectate
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in tournament delegate can get spectate match page when tournament is public" do
|
||||||
|
@tournament.update!(is_public: true)
|
||||||
|
sign_in_tournament_delegate
|
||||||
|
get_spectate
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in tournament owner can get spectate match page when tournament is public" do
|
||||||
|
@tournament.update!(is_public: true)
|
||||||
|
sign_in_owner
|
||||||
|
get_spectate
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non logged in user can get spectate match page when tournament is public" do
|
||||||
|
@tournament.update!(is_public: true)
|
||||||
|
get_spectate
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "spectate renders embedded scoreboard when match is unfinished" do
|
||||||
|
@tournament.update!(is_public: true)
|
||||||
|
@match.update!(finished: nil)
|
||||||
|
|
||||||
|
get_spectate
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
assert_includes response.body, "data-match-spectate-target=\"scoreboardContainer\""
|
||||||
|
assert_includes response.body, "data-controller=\"match-scoreboard\""
|
||||||
|
assert_includes response.body, "data-match-scoreboard-source-mode-value=\"websocket\""
|
||||||
|
assert_includes response.body, "data-match-scoreboard-display-mode-value=\"embedded\""
|
||||||
|
assert_includes response.body, "data-match-scoreboard-target=\"greenTimerIndicator\""
|
||||||
|
assert_includes response.body, "data-match-scoreboard-target=\"redTimerIndicator\""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "spectate hides embedded scoreboard when match is finished" do
|
||||||
|
@tournament.update!(is_public: true)
|
||||||
|
@match.update!(finished: 1, winner_id: @match.w1, win_type: "Decision", score: "3-1")
|
||||||
|
|
||||||
|
get_spectate
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
assert_not_includes response.body, "data-match-spectate-target=\"scoreboardContainer\""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "posting a match update from match state redirects to all matches" do
|
||||||
|
sign_in_owner
|
||||||
|
get :state, params: { id: @match.id }
|
||||||
|
patch :update, params: { id: @match.id, match: { score: "3-1", win_type: "Decision", winner_id: @match.w1, finished: 1 } }
|
||||||
|
|
||||||
|
assert_redirected_to "/tournaments/#{@tournament.id}/matches"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "state page renders hidden stat fields for generated stats submission" do
|
||||||
|
sign_in_owner
|
||||||
|
get_state
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
assert_includes response.body, 'name="match[w1_stat]"'
|
||||||
|
assert_includes response.body, 'name="match[w2_stat]"'
|
||||||
|
assert_includes response.body, 'data-match-state-target="w1StatField"'
|
||||||
|
assert_includes response.body, 'data-match-state-target="w2StatField"'
|
||||||
|
end
|
||||||
|
|
||||||
|
test "posting a match update from match state respects redirect_to param" do
|
||||||
|
sign_in_owner
|
||||||
|
get :state, params: { id: @match.id, redirect_to: "/mats/#{@match.mat_id}" }
|
||||||
|
patch :update, params: {
|
||||||
|
id: @match.id,
|
||||||
|
redirect_to: "/mats/#{@match.mat_id}",
|
||||||
|
match: { score: "3-1", win_type: "Decision", winner_id: @match.w1, finished: 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_redirected_to "/mats/#{@match.mat_id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "posting a match update broadcasts match data and cached scoreboard state" do
|
||||||
|
sign_in_owner
|
||||||
|
scoreboard_state = {
|
||||||
|
"participantScores" => { "w1" => 3, "w2" => 1 },
|
||||||
|
"metadata" => { "boutNumber" => @match.bout_number }
|
||||||
|
}
|
||||||
|
stream = MatchChannel.broadcasting_for(@match)
|
||||||
|
ActionCable.server.pubsub.clear
|
||||||
|
ActionCable.server.pubsub.broadcasts(stream).clear
|
||||||
|
Rails.cache.write(
|
||||||
|
"tournament:#{@match.tournament_id}:match:#{@match.id}:scoreboard_state",
|
||||||
|
scoreboard_state
|
||||||
|
)
|
||||||
|
|
||||||
|
patch :update, params: {
|
||||||
|
id: @match.id,
|
||||||
|
match: {
|
||||||
|
w1_stat: "T3",
|
||||||
|
w2_stat: "E1",
|
||||||
|
score: "3-1",
|
||||||
|
win_type: "Decision",
|
||||||
|
winner_id: @match.w1,
|
||||||
|
finished: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = JSON.parse(ActionCable.server.pubsub.broadcasts(stream).last)
|
||||||
|
assert_equal(
|
||||||
|
{
|
||||||
|
"w1_stat" => "T3",
|
||||||
|
"w2_stat" => "E1",
|
||||||
|
"score" => "3-1",
|
||||||
|
"win_type" => "Decision",
|
||||||
|
"winner_id" => @match.w1,
|
||||||
|
"winner_name" => @match.wrestler1.name,
|
||||||
|
"finished" => 1,
|
||||||
|
"scoreboard_state" => scoreboard_state
|
||||||
|
},
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
test "logged in tournament delegate should post update match" do
|
test "logged in tournament delegate should post update match" do
|
||||||
sign_in_tournament_delegate
|
sign_in_tournament_delegate
|
||||||
post_update
|
post_update
|
||||||
|
|||||||
@@ -31,6 +31,30 @@ class MatsControllerTest < ActionController::TestCase
|
|||||||
get :show, params: { id: 1 }
|
get :show, params: { id: 1 }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_state
|
||||||
|
get :state, params: { id: @mat.id }
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_state_with_params(extra_params = {})
|
||||||
|
get :state, params: { id: @mat.id }.merge(extra_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_scoreboard
|
||||||
|
get :scoreboard, params: { id: @mat.id }
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_scoreboard_with_params(extra_params = {})
|
||||||
|
get :scoreboard, params: { id: @mat.id }.merge(extra_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def post_select_match(extra_params = {})
|
||||||
|
post :select_match, params: { id: @mat.id, match_id: @match.id, bout_number: @match.bout_number }.merge(extra_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def post_select_match_with_params(extra_params = {})
|
||||||
|
post :select_match, params: { id: @mat.id }.merge(extra_params)
|
||||||
|
end
|
||||||
|
|
||||||
def post_update
|
def post_update
|
||||||
patch :update, params: { id: @mat.id, mat: {name: @mat.name, tournament_id: @mat.tournament_id} }
|
patch :update, params: { id: @mat.id, mat: {name: @mat.name, tournament_id: @mat.tournament_id} }
|
||||||
end
|
end
|
||||||
@@ -212,24 +236,253 @@ class MatsControllerTest < ActionController::TestCase
|
|||||||
redirect
|
redirect
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "logged in user should not get state mat page" do
|
||||||
|
sign_in_non_owner
|
||||||
|
get_state
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in user should not get scoreboard mat page" do
|
||||||
|
sign_in_non_owner
|
||||||
|
get_scoreboard
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
test "logged school delegate should not get show mat" do
|
test "logged school delegate should not get show mat" do
|
||||||
sign_in_school_delegate
|
sign_in_school_delegate
|
||||||
show
|
show
|
||||||
redirect
|
redirect
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "logged school delegate should not get state mat page" do
|
||||||
|
sign_in_school_delegate
|
||||||
|
get_state
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged school delegate should not get scoreboard mat page" do
|
||||||
|
sign_in_school_delegate
|
||||||
|
get_scoreboard
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non logged in user should not get state mat page" do
|
||||||
|
get_state
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non logged in user should not get scoreboard mat page" do
|
||||||
|
get_scoreboard
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
|
test "valid school permission key cannot get state mat page" do
|
||||||
|
school = @tournament.schools.first
|
||||||
|
school.update!(permission_key: "valid-school-key")
|
||||||
|
|
||||||
|
get_state_with_params(school_permission_key: "valid-school-key")
|
||||||
|
assert_redirected_to "/static_pages/not_allowed"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invalid school permission key cannot get state mat page" do
|
||||||
|
school = @tournament.schools.first
|
||||||
|
school.update!(permission_key: "valid-school-key")
|
||||||
|
|
||||||
|
get_state_with_params(school_permission_key: "invalid-school-key")
|
||||||
|
assert_redirected_to "/static_pages/not_allowed"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "valid school permission key cannot get scoreboard mat page" do
|
||||||
|
school = @tournament.schools.first
|
||||||
|
school.update!(permission_key: "valid-school-key")
|
||||||
|
|
||||||
|
get_scoreboard_with_params(school_permission_key: "valid-school-key")
|
||||||
|
assert_redirected_to "/static_pages/not_allowed"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invalid school permission key cannot get scoreboard mat page" do
|
||||||
|
school = @tournament.schools.first
|
||||||
|
school.update!(permission_key: "valid-school-key")
|
||||||
|
|
||||||
|
get_scoreboard_with_params(school_permission_key: "invalid-school-key")
|
||||||
|
assert_redirected_to "/static_pages/not_allowed"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in user should not post select_match on mat" do
|
||||||
|
sign_in_non_owner
|
||||||
|
post_select_match
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged school delegate should not post select_match on mat" do
|
||||||
|
sign_in_school_delegate
|
||||||
|
post_select_match
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non logged in user should not post select_match on mat" do
|
||||||
|
post_select_match
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
|
test "valid school permission key cannot post select_match on mat" do
|
||||||
|
school = @tournament.schools.first
|
||||||
|
school.update!(permission_key: "valid-school-key")
|
||||||
|
|
||||||
|
post_select_match_with_params(school_permission_key: "valid-school-key")
|
||||||
|
assert_redirected_to "/static_pages/not_allowed"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invalid school permission key cannot post select_match on mat" do
|
||||||
|
school = @tournament.schools.first
|
||||||
|
school.update!(permission_key: "valid-school-key")
|
||||||
|
|
||||||
|
post_select_match_with_params(school_permission_key: "invalid-school-key")
|
||||||
|
assert_redirected_to "/static_pages/not_allowed"
|
||||||
|
end
|
||||||
|
|
||||||
test "logged in tournament owner should get show mat" do
|
test "logged in tournament owner should get show mat" do
|
||||||
sign_in_owner
|
sign_in_owner
|
||||||
show
|
show
|
||||||
success
|
success
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "logged in tournament owner should get state mat page" do
|
||||||
|
sign_in_owner
|
||||||
|
get_state
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in tournament owner should get scoreboard mat page" do
|
||||||
|
sign_in_owner
|
||||||
|
get_scoreboard
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in tournament owner can post select_match on mat" do
|
||||||
|
sign_in_owner
|
||||||
|
post_select_match
|
||||||
|
assert_response :no_content
|
||||||
|
end
|
||||||
|
|
||||||
test "logged in tournament delegate should get show mat" do
|
test "logged in tournament delegate should get show mat" do
|
||||||
sign_in_tournament_delegate
|
sign_in_tournament_delegate
|
||||||
show
|
show
|
||||||
success
|
success
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "logged in tournament delegate should get state mat page" do
|
||||||
|
sign_in_tournament_delegate
|
||||||
|
get_state
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in tournament delegate should get scoreboard mat page" do
|
||||||
|
sign_in_tournament_delegate
|
||||||
|
get_scoreboard
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "state mat page renders queue buttons and mat-state controller" do
|
||||||
|
sign_in_owner
|
||||||
|
get_state
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
assert_includes response.body, "data-controller=\"mat-state\""
|
||||||
|
assert_includes response.body, "Queue 1:"
|
||||||
|
assert_includes response.body, "Queue 2:"
|
||||||
|
assert_includes response.body, "Queue 3:"
|
||||||
|
assert_includes response.body, "Queue 4:"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "scoreboard mat page renders match-scoreboard controller" do
|
||||||
|
sign_in_owner
|
||||||
|
get_scoreboard_with_params(print: true)
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
assert_includes response.body, "data-controller=\"match-scoreboard\""
|
||||||
|
assert_includes response.body, "data-match-scoreboard-source-mode-value=\"localstorage\""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "scoreboard mat page uses selected scoreboard match as initial bout" do
|
||||||
|
sign_in_owner
|
||||||
|
alternate_match = @mat.queue2_match
|
||||||
|
if alternate_match.nil?
|
||||||
|
alternate_match = @tournament.matches.where(mat_id: nil).first
|
||||||
|
@mat.assign_match_to_queue!(alternate_match, 2)
|
||||||
|
alternate_match = @mat.reload.queue2_match
|
||||||
|
end
|
||||||
|
@mat.set_selected_scoreboard_match!(alternate_match)
|
||||||
|
|
||||||
|
get_scoreboard
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
assert_includes response.body, "data-match-scoreboard-initial-bout-number-value=\"#{alternate_match.bout_number}\""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "state mat page renders no matches assigned when queue is empty" do
|
||||||
|
sign_in_owner
|
||||||
|
@mat.clear_queue!
|
||||||
|
|
||||||
|
get_state
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
assert_includes response.body, "No matches assigned to this mat."
|
||||||
|
end
|
||||||
|
|
||||||
|
test "posting a match update from mat state redirects back to mat state" do
|
||||||
|
sign_in_owner
|
||||||
|
get :state, params: { id: @mat.id, bout_number: @match.bout_number }
|
||||||
|
|
||||||
|
old_controller = @controller
|
||||||
|
@controller = MatchesController.new
|
||||||
|
patch :update, params: { id: @match.id, match: { score: "3-1", win_type: "Decision", winner_id: @match.w1, finished: 1 } }
|
||||||
|
@controller = old_controller
|
||||||
|
|
||||||
|
assert_redirected_to "/mats/#{@mat.id}/state"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in tournament delegate can post select_match on mat" do
|
||||||
|
sign_in_tournament_delegate
|
||||||
|
post_select_match
|
||||||
|
assert_response :no_content
|
||||||
|
end
|
||||||
|
|
||||||
|
test "select_match updates selected scoreboard match" do
|
||||||
|
sign_in_owner
|
||||||
|
alternate_match = @mat.queue2_match
|
||||||
|
if alternate_match.nil?
|
||||||
|
alternate_match = @tournament.matches.where(mat_id: nil).first
|
||||||
|
@mat.assign_match_to_queue!(alternate_match, 2)
|
||||||
|
alternate_match = @mat.reload.queue2_match
|
||||||
|
end
|
||||||
|
|
||||||
|
post :select_match, params: { id: @mat.id, match_id: alternate_match.id, bout_number: alternate_match.bout_number }
|
||||||
|
|
||||||
|
assert_response :no_content
|
||||||
|
assert_equal alternate_match.id, @mat.selected_scoreboard_match&.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "select_match updates last match result without changing selected match" do
|
||||||
|
sign_in_owner
|
||||||
|
@mat.set_selected_scoreboard_match!(@match)
|
||||||
|
|
||||||
|
post :select_match, params: { id: @mat.id, last_match_result: "106 lbs - Winner Decision Loser 3-1" }
|
||||||
|
|
||||||
|
assert_response :no_content
|
||||||
|
assert_equal @match.id, @mat.selected_scoreboard_match&.id
|
||||||
|
assert_equal "106 lbs - Winner Decision Loser 3-1", @mat.last_match_result_text
|
||||||
|
end
|
||||||
|
|
||||||
|
test "select_match returns unprocessable entity for a non queued match without last result" do
|
||||||
|
sign_in_owner
|
||||||
|
non_queued_match = @tournament.matches.where(mat_id: nil).first
|
||||||
|
|
||||||
|
post :select_match, params: { id: @mat.id, match_id: non_queued_match.id, bout_number: non_queued_match.bout_number }
|
||||||
|
|
||||||
|
assert_response :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
test "ads are hidden on mat show" do
|
test "ads are hidden on mat show" do
|
||||||
sign_in_owner
|
sign_in_owner
|
||||||
show
|
show
|
||||||
@@ -259,6 +512,62 @@ class MatsControllerTest < ActionController::TestCase
|
|||||||
assert_match /#{bout_number}/, response.body, "The bout_number should be rendered on the page"
|
assert_match /#{bout_number}/, response.body, "The bout_number should be rendered on the page"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "mat show renders queue buttons for all four queue slots" do
|
||||||
|
sign_in_owner
|
||||||
|
|
||||||
|
available_matches = @tournament.matches.where(mat_id: nil).limit(3).to_a
|
||||||
|
@mat.assign_match_to_queue!(available_matches[0], 2) if available_matches[0]
|
||||||
|
@mat.assign_match_to_queue!(available_matches[1], 3) if available_matches[1]
|
||||||
|
@mat.assign_match_to_queue!(available_matches[2], 4) if available_matches[2]
|
||||||
|
|
||||||
|
get :show, params: { id: @mat.id }
|
||||||
|
assert_response :success
|
||||||
|
|
||||||
|
assert_includes response.body, "Queue 1: Bout"
|
||||||
|
assert_includes response.body, "Queue 2:"
|
||||||
|
assert_includes response.body, "Queue 3:"
|
||||||
|
assert_includes response.body, "Queue 4:"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "mat show highlights selected queue button and keeps bout_number links working" do
|
||||||
|
sign_in_owner
|
||||||
|
|
||||||
|
queue2_match = @mat.queue2_match
|
||||||
|
unless queue2_match
|
||||||
|
assignable = @tournament.matches.where(mat_id: nil).first
|
||||||
|
@mat.assign_match_to_queue!(assignable, 2) if assignable
|
||||||
|
queue2_match = @mat.reload.queue2_match
|
||||||
|
end
|
||||||
|
assert queue2_match, "Expected queue2 match to be present"
|
||||||
|
|
||||||
|
get :show, params: { id: @mat.id, bout_number: queue2_match.bout_number, foo: "bar" }
|
||||||
|
assert_response :success
|
||||||
|
|
||||||
|
assert_includes response.body, "Queue 2: Bout #{queue2_match.bout_number}"
|
||||||
|
assert_match(/btn btn-success btn-sm/, response.body)
|
||||||
|
assert_includes response.body, "bout_number=#{queue2_match.bout_number}"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "mat show falls back to queue1 when requested bout number is not currently queued" do
|
||||||
|
sign_in_owner
|
||||||
|
|
||||||
|
queue1 = @mat.reload.queue1_match
|
||||||
|
assert queue1, "Expected queue1 to be present"
|
||||||
|
|
||||||
|
get :show, params: { id: @mat.id, bout_number: 999999 }
|
||||||
|
assert_response :success
|
||||||
|
assert_includes response.body, "Bout <strong>#{queue1.bout_number}</strong>"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "mat show renders no matches assigned when queue is empty" do
|
||||||
|
sign_in_owner
|
||||||
|
|
||||||
|
@mat.clear_queue!
|
||||||
|
get :show, params: { id: @mat.id }
|
||||||
|
assert_response :success
|
||||||
|
assert_includes response.body, "No matches assigned to this mat."
|
||||||
|
end
|
||||||
|
|
||||||
test "logged in tournament owner should redirect back to the first unfinished bout on a mat after submitting a match with a bout number param" do
|
test "logged in tournament owner should redirect back to the first unfinished bout on a mat after submitting a match with a bout number param" do
|
||||||
sign_in_owner
|
sign_in_owner
|
||||||
|
|
||||||
@@ -287,11 +596,12 @@ class MatsControllerTest < ActionController::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
#TESTS THAT NEED MATCHES PUT ABOVE THIS
|
#TESTS THAT NEED MATCHES PUT ABOVE THIS
|
||||||
test "redirect show if no matches" do
|
test "show renders when no matches" do
|
||||||
sign_in_owner
|
sign_in_owner
|
||||||
wipe
|
wipe
|
||||||
show
|
show
|
||||||
no_matches
|
success
|
||||||
|
assert_includes response.body, "No matches assigned to this mat."
|
||||||
end
|
end
|
||||||
|
|
||||||
# Assign Next Match Permissions
|
# Assign Next Match Permissions
|
||||||
|
|||||||
143
test/controllers/school_show_cache_test.rb
Normal file
143
test/controllers/school_show_cache_test.rb
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class SchoolShowCacheTest < ActionController::TestCase
|
||||||
|
tests SchoolsController
|
||||||
|
|
||||||
|
setup do
|
||||||
|
create_double_elim_tournament_single_weight_1_6(8)
|
||||||
|
@tournament.update!(user_id: users(:one).id)
|
||||||
|
@school = @tournament.schools.first
|
||||||
|
|
||||||
|
sign_in users(:one)
|
||||||
|
|
||||||
|
@original_perform_caching = ActionController::Base.perform_caching
|
||||||
|
ActionController::Base.perform_caching = true
|
||||||
|
Rails.cache.clear
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
Rails.cache.clear
|
||||||
|
ActionController::Base.perform_caching = @original_perform_caching
|
||||||
|
end
|
||||||
|
|
||||||
|
test "school show wrestler cell fragments hit cache and invalidate after wrestler update" do
|
||||||
|
first_events = cache_events_for_school_show do
|
||||||
|
get :show, params: { id: @school.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(first_events), :>, 0, "Expected initial school show render to write wrestler cell fragments"
|
||||||
|
|
||||||
|
second_events = cache_events_for_school_show do
|
||||||
|
get :show, params: { id: @school.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_equal 0, cache_writes(second_events), "Expected repeat school show render to reuse wrestler cell fragments"
|
||||||
|
assert_operator cache_hits(second_events), :>, 0, "Expected repeat school show render to hit wrestler cell cache"
|
||||||
|
|
||||||
|
wrestler = @school.wrestlers.first
|
||||||
|
third_events = cache_events_for_school_show do
|
||||||
|
wrestler.touch
|
||||||
|
get :show, params: { id: @school.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(third_events), :>, 0, "Expected wrestler update to invalidate school show wrestler cell cache"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "school show does not leak manage-only controls from cache across users" do
|
||||||
|
get :show, params: { id: @school.id }
|
||||||
|
assert_response :success
|
||||||
|
assert_includes response.body, "New Wrestler"
|
||||||
|
assert_match(/fa-trash-alt/, response.body)
|
||||||
|
assert_match(/fa-edit/, response.body)
|
||||||
|
|
||||||
|
sign_out
|
||||||
|
|
||||||
|
spectator_events = cache_events_for_school_show do
|
||||||
|
get :show, params: { id: @school.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_hits(spectator_events), :>, 0, "Expected spectator request to hit wrestler cell cache warmed by owner"
|
||||||
|
assert_not_includes response.body, "New Wrestler"
|
||||||
|
assert_no_match(/fa-trash-alt/, response.body)
|
||||||
|
assert_no_match(/fa-edit/, response.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "school show with school_permission_key bypasses cached wrestler cell fragments" do
|
||||||
|
@school.update!(permission_key: SecureRandom.uuid)
|
||||||
|
sign_out
|
||||||
|
|
||||||
|
key_request_events = cache_events_for_school_show do
|
||||||
|
get :show, params: { id: @school.id, school_permission_key: @school.permission_key }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_equal 0, cache_writes(key_request_events), "Expected school_permission_key request to bypass cached wrestler cells"
|
||||||
|
assert_equal 0, cache_hits(key_request_events), "Expected school_permission_key request to avoid reading cached wrestler cells"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "completing a match expires school show wrestler cell caches" do
|
||||||
|
warm_events = cache_events_for_school_show do
|
||||||
|
get :show, params: { id: @school.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(warm_events), :>, 0, "Expected initial school show render to warm wrestler cell cache"
|
||||||
|
|
||||||
|
wrestler = @school.wrestlers.first
|
||||||
|
assert wrestler, "Expected a wrestler for match-completion cache test"
|
||||||
|
match = wrestler.unfinished_matches.first || wrestler.all_matches.first
|
||||||
|
assert match, "Expected a match involving school wrestler"
|
||||||
|
|
||||||
|
winner_id = match.w1 || match.w2
|
||||||
|
assert winner_id, "Expected match to have at least one wrestler slot"
|
||||||
|
match.update!(
|
||||||
|
finished: 1,
|
||||||
|
winner_id: winner_id,
|
||||||
|
win_type: "Decision",
|
||||||
|
score: "1-0"
|
||||||
|
)
|
||||||
|
|
||||||
|
post_action_events = cache_events_for_school_show do
|
||||||
|
get :show, params: { id: @school.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(post_action_events), :>, 0, "Expected completed match to expire school show wrestler cell cache"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def sign_out
|
||||||
|
@request.session[:user_id] = nil
|
||||||
|
@controller.instance_variable_set(:@current_user, nil)
|
||||||
|
@controller.instance_variable_set(:@current_ability, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_events_for_school_show
|
||||||
|
events = []
|
||||||
|
subscriber = lambda do |name, _start, _finish, _id, payload|
|
||||||
|
key = payload[:key].to_s
|
||||||
|
next unless key.include?("school_show_wrestler_cells")
|
||||||
|
|
||||||
|
events << { name: name, hit: payload[:hit] }
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveSupport::Notifications.subscribed(
|
||||||
|
subscriber,
|
||||||
|
/cache_(read|write|fetch_hit|generate)\.active_support/
|
||||||
|
) do
|
||||||
|
yield
|
||||||
|
end
|
||||||
|
|
||||||
|
events
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_writes(events)
|
||||||
|
events.count { |event| event[:name] == "cache_write.active_support" }
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_hits(events)
|
||||||
|
events.count do |event|
|
||||||
|
event[:name] == "cache_fetch_hit.active_support" ||
|
||||||
|
(event[:name] == "cache_read.active_support" && event[:hit])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -36,4 +36,41 @@ class StaticPagesControllerTest < ActionController::TestCase
|
|||||||
get :my_tournaments
|
get :my_tournaments
|
||||||
success
|
success
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "my_tournaments page lists delegated tournament and delegated school once in html" do
|
||||||
|
user = users(:two)
|
||||||
|
sign_in_non_owner
|
||||||
|
|
||||||
|
delegated_tournament = Tournament.create!(
|
||||||
|
name: "Delegated Tournament #{SecureRandom.hex(4)}",
|
||||||
|
address: "123 Delegate St",
|
||||||
|
director: "Director",
|
||||||
|
director_email: "delegate_tournament_#{SecureRandom.hex(4)}@example.com",
|
||||||
|
tournament_type: "Pool to bracket",
|
||||||
|
date: Date.today,
|
||||||
|
is_public: true
|
||||||
|
)
|
||||||
|
TournamentDelegate.create!(tournament_id: delegated_tournament.id, user_id: user.id)
|
||||||
|
|
||||||
|
school_tournament = Tournament.create!(
|
||||||
|
name: "School Tournament #{SecureRandom.hex(4)}",
|
||||||
|
address: "456 School St",
|
||||||
|
director: "Director",
|
||||||
|
director_email: "delegate_school_#{SecureRandom.hex(4)}@example.com",
|
||||||
|
tournament_type: "Pool to bracket",
|
||||||
|
date: Date.today + 1,
|
||||||
|
is_public: true
|
||||||
|
)
|
||||||
|
delegated_school = School.create!(
|
||||||
|
name: "Delegated School #{SecureRandom.hex(4)}",
|
||||||
|
tournament_id: school_tournament.id
|
||||||
|
)
|
||||||
|
SchoolDelegate.create!(school_id: delegated_school.id, user_id: user.id)
|
||||||
|
|
||||||
|
get :my_tournaments
|
||||||
|
assert_response :success
|
||||||
|
assert_equal 1, response.body.scan(delegated_tournament.name).size
|
||||||
|
assert_equal 1, response.body.scan(delegated_school.name).size
|
||||||
|
assert_equal 1, response.body.scan(school_tournament.name).size
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
164
test/controllers/tournament_pages_cache_test.rb
Normal file
164
test/controllers/tournament_pages_cache_test.rb
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class TournamentPagesCacheTest < ActionController::TestCase
|
||||||
|
tests TournamentsController
|
||||||
|
|
||||||
|
setup do
|
||||||
|
create_double_elim_tournament_single_weight_1_6(8)
|
||||||
|
@tournament.update!(user_id: users(:one).id)
|
||||||
|
@weight = @tournament.weights.first
|
||||||
|
|
||||||
|
sign_in users(:one)
|
||||||
|
|
||||||
|
@original_perform_caching = ActionController::Base.perform_caching
|
||||||
|
ActionController::Base.perform_caching = true
|
||||||
|
Rails.cache.clear
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
Rails.cache.clear
|
||||||
|
ActionController::Base.perform_caching = @original_perform_caching
|
||||||
|
end
|
||||||
|
|
||||||
|
test "team_scores cache hits on repeat render and rewrites after school update" do
|
||||||
|
first_events = cache_events_for(%w[team_scores team_score_row]) do
|
||||||
|
get :team_scores, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(first_events), :>, 0, "Expected initial team_scores render to write fragments"
|
||||||
|
|
||||||
|
second_events = cache_events_for(%w[team_scores team_score_row]) do
|
||||||
|
get :team_scores, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_equal 0, cache_writes(second_events), "Expected repeat team_scores render to reuse fragments"
|
||||||
|
assert_operator cache_hits(second_events), :>, 0, "Expected repeat team_scores render to hit cache"
|
||||||
|
|
||||||
|
school = @tournament.schools.first
|
||||||
|
third_events = cache_events_for(%w[team_scores team_score_row]) do
|
||||||
|
school.update!(score: (school.score || 0) + 1)
|
||||||
|
get :team_scores, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(third_events), :>, 0, "Expected school score update to invalidate team_scores cache"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "bracket cache hits on repeat render and rewrites after match update" do
|
||||||
|
key_markers = [@weight.id.to_s + "_bracket", "bracket_round_match", "bracket_final_match"]
|
||||||
|
|
||||||
|
first_events = cache_events_for(key_markers) do
|
||||||
|
get :bracket, params: { id: @tournament.id, weight: @weight.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(first_events), :>, 0, "Expected initial bracket render to write fragments"
|
||||||
|
|
||||||
|
second_events = cache_events_for(key_markers) do
|
||||||
|
get :bracket, params: { id: @tournament.id, weight: @weight.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_equal 0, cache_writes(second_events), "Expected repeat bracket render to reuse fragments"
|
||||||
|
assert_operator cache_hits(second_events), :>, 0, "Expected repeat bracket render to hit cache"
|
||||||
|
|
||||||
|
match = @weight.matches.first
|
||||||
|
third_events = cache_events_for(key_markers) do
|
||||||
|
match.touch
|
||||||
|
get :bracket, params: { id: @tournament.id, weight: @weight.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(third_events), :>, 0, "Expected match update to invalidate bracket cache"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "bracket cache separates print and non-print variants" do
|
||||||
|
key_markers = [@weight.id.to_s + "_bracket"]
|
||||||
|
|
||||||
|
non_print_events = cache_events_for(key_markers) do
|
||||||
|
get :bracket, params: { id: @tournament.id, weight: @weight.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(non_print_events), :>, 0, "Expected non-print bracket render to write a page fragment"
|
||||||
|
assert_match(%r{\/matches\/\d+\/spectate}, response.body, "Expected non-print bracket view to include spectate links")
|
||||||
|
|
||||||
|
first_print_events = cache_events_for(key_markers) do
|
||||||
|
get :bracket, params: { id: @tournament.id, weight: @weight.id, print: true }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(first_print_events), :>, 0, "Expected first print bracket render to write a separate page fragment"
|
||||||
|
assert_no_match(%r{\/matches\/\d+\/spectate}, response.body, "Expected print bracket view to omit spectate links")
|
||||||
|
|
||||||
|
second_print_events = cache_events_for(key_markers) do
|
||||||
|
get :bracket, params: { id: @tournament.id, weight: @weight.id, print: true }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_equal 0, cache_writes(second_print_events), "Expected repeat print bracket render to reuse print cache fragment"
|
||||||
|
assert_operator cache_hits(second_print_events), :>, 0, "Expected repeat print bracket render to hit cache"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "completing a match expires team_scores and bracket caches" do
|
||||||
|
team_warm_events = cache_events_for(%w[team_scores team_score_row]) do
|
||||||
|
get :team_scores, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(team_warm_events), :>, 0, "Expected initial team_scores render to warm cache"
|
||||||
|
|
||||||
|
bracket_key_markers = [@weight.id.to_s + "_bracket", "bracket_round_match", "bracket_final_match"]
|
||||||
|
bracket_warm_events = cache_events_for(bracket_key_markers) do
|
||||||
|
get :bracket, params: { id: @tournament.id, weight: @weight.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(bracket_warm_events), :>, 0, "Expected initial bracket render to warm cache"
|
||||||
|
|
||||||
|
match = @weight.matches.where(finished: [nil, 0]).first || @weight.matches.first
|
||||||
|
assert match, "Expected a match to complete for expiration test"
|
||||||
|
|
||||||
|
match.update!(
|
||||||
|
finished: 1,
|
||||||
|
winner_id: match.w1 || match.w2,
|
||||||
|
win_type: "Decision",
|
||||||
|
score: "1-0"
|
||||||
|
)
|
||||||
|
|
||||||
|
team_post_events = cache_events_for(%w[team_scores team_score_row]) do
|
||||||
|
get :team_scores, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(team_post_events), :>, 0, "Expected completed match to expire team_scores cache"
|
||||||
|
|
||||||
|
bracket_post_events = cache_events_for(bracket_key_markers) do
|
||||||
|
get :bracket, params: { id: @tournament.id, weight: @weight.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(bracket_post_events), :>, 0, "Expected completed match to expire bracket cache"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def cache_events_for(key_markers)
|
||||||
|
events = []
|
||||||
|
subscriber = lambda do |name, _start, _finish, _id, payload|
|
||||||
|
key = payload[:key].to_s
|
||||||
|
next unless key_markers.any? { |marker| key.include?(marker) }
|
||||||
|
|
||||||
|
events << { name: name, hit: payload[:hit] }
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveSupport::Notifications.subscribed(
|
||||||
|
subscriber,
|
||||||
|
/cache_(read|write|fetch_hit|generate)\.active_support/
|
||||||
|
) do
|
||||||
|
yield
|
||||||
|
end
|
||||||
|
|
||||||
|
events
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_writes(events)
|
||||||
|
events.count { |event| event[:name] == "cache_write.active_support" }
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_hits(events)
|
||||||
|
events.count do |event|
|
||||||
|
event[:name] == "cache_fetch_hit.active_support" ||
|
||||||
|
(event[:name] == "cache_read.active_support" && event[:hit])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -28,6 +28,10 @@ class TournamentsControllerTest < ActionController::TestCase
|
|||||||
get :up_matches, params: { id: 1 }
|
get :up_matches, params: { id: 1 }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_live_scores
|
||||||
|
get :live_scores, params: { id: 1 }
|
||||||
|
end
|
||||||
|
|
||||||
def get_qrcode(params = {})
|
def get_qrcode(params = {})
|
||||||
get :qrcode, params: { id: 1 }.merge(params)
|
get :qrcode, params: { id: 1 }.merge(params)
|
||||||
end
|
end
|
||||||
@@ -117,6 +121,29 @@ class TournamentsControllerTest < ActionController::TestCase
|
|||||||
sign_in_owner
|
sign_in_owner
|
||||||
get :weigh_in, params: { id: 1 }
|
get :weigh_in, params: { id: 1 }
|
||||||
success
|
success
|
||||||
|
assert_not_includes response.body, "Weights were successfully recorded."
|
||||||
|
end
|
||||||
|
|
||||||
|
test "printable weigh in sheet includes wrestler name school weight class and actual weight" do
|
||||||
|
sign_in_owner
|
||||||
|
@tournament.update!(weigh_in_ref: "Ref Smith")
|
||||||
|
wrestler = @tournament.weights.first.wrestlers.first
|
||||||
|
wrestler.update!(
|
||||||
|
name: "Printable Test Wrestler",
|
||||||
|
offical_weight: 106.4
|
||||||
|
)
|
||||||
|
school = wrestler.school
|
||||||
|
|
||||||
|
get :weigh_in_sheet, params: { id: @tournament.id, print: true }
|
||||||
|
assert_response :success
|
||||||
|
|
||||||
|
assert_includes response.body, school.name
|
||||||
|
assert_includes response.body, "Printable Test Wrestler"
|
||||||
|
assert_includes response.body, wrestler.weight.max.to_s
|
||||||
|
assert_includes response.body, "106.4"
|
||||||
|
assert_includes response.body, "Actual Weight"
|
||||||
|
assert_includes response.body, "Weigh In Ref:"
|
||||||
|
assert_includes response.body, "Ref Smith"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "logged in non tournament owner cannot access weigh_ins" do
|
test "logged in non tournament owner cannot access weigh_ins" do
|
||||||
@@ -155,6 +182,27 @@ class TournamentsControllerTest < ActionController::TestCase
|
|||||||
success
|
success
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "logged in tournament owner can save wrestler actual weight on weigh in weight page" do
|
||||||
|
sign_in_owner
|
||||||
|
wrestler = @tournament.weights.first.wrestlers.first
|
||||||
|
|
||||||
|
post :weigh_in_weight, params: {
|
||||||
|
id: @tournament.id,
|
||||||
|
weight: wrestler.weight_id,
|
||||||
|
wrestler: {
|
||||||
|
wrestler.id.to_s => { offical_weight: "108.2" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_redirected_to "/tournaments/#{@tournament.id}/weigh_in/#{wrestler.weight_id}"
|
||||||
|
assert_equal "Weights were successfully recorded.", flash[:notice]
|
||||||
|
assert_equal 108.2, wrestler.reload.offical_weight.to_f
|
||||||
|
|
||||||
|
get :weigh_in_weight, params: { id: @tournament.id, weight: wrestler.weight_id }
|
||||||
|
assert_response :success
|
||||||
|
assert_equal 1, response.body.scan("Weights were successfully recorded.").size
|
||||||
|
end
|
||||||
|
|
||||||
test "logged in non tournament owner cannot access post weigh_in_weight" do
|
test "logged in non tournament owner cannot access post weigh_in_weight" do
|
||||||
sign_in_non_owner
|
sign_in_non_owner
|
||||||
post :weigh_in_weight, params: { id: 1, weight: 1, wrestler: @wrestlers }
|
post :weigh_in_weight, params: { id: 1, weight: 1, wrestler: @wrestlers }
|
||||||
@@ -515,8 +563,148 @@ class TournamentsControllerTest < ActionController::TestCase
|
|||||||
get_up_matches
|
get_up_matches
|
||||||
success
|
success
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "up matches uses turbo stream updates instead of timer refresh script" do
|
||||||
|
@tournament.is_public = true
|
||||||
|
@tournament.save
|
||||||
|
get_up_matches
|
||||||
|
success
|
||||||
|
assert_includes response.body, "turbo-cable-stream-source"
|
||||||
|
assert_includes response.body, "data-controller=\"up-matches-connection\""
|
||||||
|
assert_includes response.body, "up-matches-cable-status-indicator"
|
||||||
|
assert_not_includes response.body, "This page reloads every 30s"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "up matches shows full screen button when print param is not true" do
|
||||||
|
@tournament.is_public = true
|
||||||
|
@tournament.save
|
||||||
|
get :up_matches, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
|
||||||
|
assert_includes response.body, "Show Bout Board in Full Screen"
|
||||||
|
assert_includes response.body, "print=true"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "up matches hides full screen button when print param is true" do
|
||||||
|
@tournament.is_public = true
|
||||||
|
@tournament.save
|
||||||
|
get :up_matches, params: { id: @tournament.id, print: "true" }
|
||||||
|
assert_response :success
|
||||||
|
|
||||||
|
assert_not_includes response.body, "Show Bout Board in Full Screen"
|
||||||
|
end
|
||||||
# END UP MATCHES PAGE PERMISSIONS
|
# END UP MATCHES PAGE PERMISSIONS
|
||||||
|
|
||||||
|
# LIVE SCORES PAGE PERMISSIONS WHEN TOURNAMENT IS NOT PUBLIC
|
||||||
|
test "logged in school delegate cannot get live scores page when tournament is not public" do
|
||||||
|
@tournament.is_public = false
|
||||||
|
@tournament.save
|
||||||
|
sign_in_school_delegate
|
||||||
|
get_live_scores
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in user cannot get live scores page when tournament is not public" do
|
||||||
|
@tournament.is_public = false
|
||||||
|
@tournament.save
|
||||||
|
sign_in_non_owner
|
||||||
|
get_live_scores
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in tournament delegate can get live scores page when tournament is not public" do
|
||||||
|
@tournament.is_public = false
|
||||||
|
@tournament.save
|
||||||
|
sign_in_delegate
|
||||||
|
get_live_scores
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in tournament owner can get live scores page when tournament is not public" do
|
||||||
|
@tournament.is_public = false
|
||||||
|
@tournament.save
|
||||||
|
sign_in_owner
|
||||||
|
get_live_scores
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non logged in user cannot get live scores page when tournament is not public" do
|
||||||
|
@tournament.is_public = false
|
||||||
|
@tournament.save
|
||||||
|
get_live_scores
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non logged in user with valid school permission key cannot get live scores page when tournament is not public" do
|
||||||
|
@tournament.is_public = false
|
||||||
|
@tournament.save
|
||||||
|
@school.update(permission_key: "valid-key")
|
||||||
|
get :live_scores, params: { id: @tournament.id, school_permission_key: "valid-key" }
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non logged in user with invalid school permission key cannot get live scores page when tournament is not public" do
|
||||||
|
@tournament.is_public = false
|
||||||
|
@tournament.save
|
||||||
|
@school.update(permission_key: "valid-key")
|
||||||
|
get :live_scores, params: { id: @tournament.id, school_permission_key: "invalid-key" }
|
||||||
|
redirect
|
||||||
|
end
|
||||||
|
|
||||||
|
# LIVE SCORES PAGE PERMISSIONS WHEN TOURNAMENT IS PUBLIC
|
||||||
|
test "logged in school delegate can get live scores page when tournament is public" do
|
||||||
|
@tournament.is_public = true
|
||||||
|
@tournament.save
|
||||||
|
sign_in_school_delegate
|
||||||
|
get_live_scores
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in user can get live scores page when tournament is public" do
|
||||||
|
@tournament.is_public = true
|
||||||
|
@tournament.save
|
||||||
|
sign_in_non_owner
|
||||||
|
get_live_scores
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in tournament delegate can get live scores page when tournament is public" do
|
||||||
|
@tournament.is_public = true
|
||||||
|
@tournament.save
|
||||||
|
sign_in_delegate
|
||||||
|
get_live_scores
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logged in tournament owner can get live scores page when tournament is public" do
|
||||||
|
@tournament.is_public = true
|
||||||
|
@tournament.save
|
||||||
|
sign_in_owner
|
||||||
|
get_live_scores
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non logged in user can get live scores page when tournament is public" do
|
||||||
|
@tournament.is_public = true
|
||||||
|
@tournament.save
|
||||||
|
get_live_scores
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "live scores page renders mat cards and scoreboard controller" do
|
||||||
|
@tournament.is_public = true
|
||||||
|
@tournament.save
|
||||||
|
get_live_scores
|
||||||
|
success
|
||||||
|
assert_includes response.body, "Live Scores"
|
||||||
|
assert_includes response.body, "data-controller=\"match-scoreboard\""
|
||||||
|
assert_includes response.body, "data-match-scoreboard-source-mode-value=\"mat_websocket\""
|
||||||
|
assert_includes response.body, "data-match-scoreboard-display-mode-value=\"embedded\""
|
||||||
|
assert_includes response.body, "Last Match Result"
|
||||||
|
assert_includes response.body, "Stats"
|
||||||
|
assert_not_includes response.body, "Result</h4>"
|
||||||
|
end
|
||||||
|
|
||||||
# ALL_RESULTS PAGE PERMISSIONS WHEN TOURNAMENT IS NOT PUBLIC
|
# ALL_RESULTS PAGE PERMISSIONS WHEN TOURNAMENT IS NOT PUBLIC
|
||||||
test "logged in school delegate cannot get all_results page when tournament is not public" do
|
test "logged in school delegate cannot get all_results page when tournament is not public" do
|
||||||
@tournament.is_public = false
|
@tournament.is_public = false
|
||||||
@@ -599,11 +787,11 @@ class TournamentsControllerTest < ActionController::TestCase
|
|||||||
# END ALL_RESULTS PAGE PERMISSIONS
|
# END ALL_RESULTS PAGE PERMISSIONS
|
||||||
|
|
||||||
#TESTS THAT NEED MATCHES PUT ABOVE THIS
|
#TESTS THAT NEED MATCHES PUT ABOVE THIS
|
||||||
test "redirect up_matches if no matches" do
|
test "up_matches renders when no matches exist" do
|
||||||
sign_in_owner
|
sign_in_owner
|
||||||
wipe
|
wipe
|
||||||
get :up_matches, params: { id: 1 }
|
get :up_matches, params: { id: 1 }
|
||||||
no_matches
|
success
|
||||||
end
|
end
|
||||||
|
|
||||||
test "redirect bracket if no matches" do
|
test "redirect bracket if no matches" do
|
||||||
@@ -686,6 +874,33 @@ class TournamentsControllerTest < ActionController::TestCase
|
|||||||
success
|
success
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "delegate page renders created tournament delegate in html" do
|
||||||
|
user = User.create!(
|
||||||
|
email: "tournament_delegate_render_#{SecureRandom.hex(4)}@example.com",
|
||||||
|
password: "password"
|
||||||
|
)
|
||||||
|
TournamentDelegate.create!(tournament_id: @tournament.id, user_id: user.id)
|
||||||
|
|
||||||
|
sign_in_owner
|
||||||
|
get :delegate, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
assert_includes response.body, user.email
|
||||||
|
end
|
||||||
|
|
||||||
|
test "school_delegate page renders created school delegate in html" do
|
||||||
|
user = User.create!(
|
||||||
|
email: "school_delegate_render_#{SecureRandom.hex(4)}@example.com",
|
||||||
|
password: "password"
|
||||||
|
)
|
||||||
|
SchoolDelegate.create!(school_id: @school.id, user_id: user.id)
|
||||||
|
|
||||||
|
sign_in_owner
|
||||||
|
get :school_delegate, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
assert_includes response.body, user.email
|
||||||
|
assert_includes response.body, @school.name
|
||||||
|
end
|
||||||
|
|
||||||
test 'logged in tournament owner can delete a school delegate' do
|
test 'logged in tournament owner can delete a school delegate' do
|
||||||
sign_in_owner
|
sign_in_owner
|
||||||
patch :remove_school_delegate, params: { id: 1, delegate: SchoolDelegate.find(1) }
|
patch :remove_school_delegate, params: { id: 1, delegate: SchoolDelegate.find(1) }
|
||||||
@@ -722,6 +937,16 @@ class TournamentsControllerTest < ActionController::TestCase
|
|||||||
success
|
success
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "teampointadjust page lists created point deduction once in html" do
|
||||||
|
sign_in_owner
|
||||||
|
school = School.create!(name: "Point Deduction School #{SecureRandom.hex(3)}", tournament_id: @tournament.id)
|
||||||
|
adjustment = Teampointadjust.create!(school_id: school.id, points: 9876.5)
|
||||||
|
|
||||||
|
get :teampointadjust, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
assert_equal 1, response.body.scan(adjustment.points.to_s).size
|
||||||
|
end
|
||||||
|
|
||||||
test 'logged in tournament delegate cannot adjust team points' do
|
test 'logged in tournament delegate cannot adjust team points' do
|
||||||
sign_in_school_delegate
|
sign_in_school_delegate
|
||||||
get :teampointadjust, params: { id: 1 }
|
get :teampointadjust, params: { id: 1 }
|
||||||
@@ -758,6 +983,83 @@ class TournamentsControllerTest < ActionController::TestCase
|
|||||||
success
|
success
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "matches page search finds by wrestler name, school name, and bout number" do
|
||||||
|
sign_in_owner
|
||||||
|
|
||||||
|
search_school = School.create!(name: "Search Prep Academy", tournament_id: @tournament.id)
|
||||||
|
search_wrestler = Wrestler.create!(
|
||||||
|
name: "Alpha Searchman",
|
||||||
|
school_id: search_school.id,
|
||||||
|
weight_id: @tournament.weights.first.id,
|
||||||
|
original_seed: 99,
|
||||||
|
bracket_line: 99,
|
||||||
|
season_loss: 0,
|
||||||
|
season_win: 0,
|
||||||
|
pool: 1
|
||||||
|
)
|
||||||
|
match = Match.create!(
|
||||||
|
tournament_id: @tournament.id,
|
||||||
|
weight_id: @tournament.weights.first.id,
|
||||||
|
bout_number: 888999,
|
||||||
|
w1: search_wrestler.id,
|
||||||
|
w2: @wrestlers.first.id,
|
||||||
|
bracket_position: "Pool",
|
||||||
|
round: 1
|
||||||
|
)
|
||||||
|
|
||||||
|
get :matches, params: { id: @tournament.id, search: "Searchman" }
|
||||||
|
assert_response :success
|
||||||
|
assert_includes response.body, match.bout_number.to_s
|
||||||
|
|
||||||
|
get :matches, params: { id: @tournament.id, search: "Search Prep" }
|
||||||
|
assert_response :success
|
||||||
|
assert_includes response.body, match.bout_number.to_s
|
||||||
|
|
||||||
|
get :matches, params: { id: @tournament.id, search: "888999" }
|
||||||
|
assert_response :success
|
||||||
|
assert_includes response.body, match.bout_number.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
test "matches page paginates filtered results" do
|
||||||
|
sign_in_owner
|
||||||
|
|
||||||
|
paging_school = School.create!(name: "Pager Academy", tournament_id: @tournament.id)
|
||||||
|
paging_wrestler = Wrestler.create!(
|
||||||
|
name: "Pager Wrestler",
|
||||||
|
school_id: paging_school.id,
|
||||||
|
weight_id: @tournament.weights.first.id,
|
||||||
|
original_seed: 100,
|
||||||
|
bracket_line: 100,
|
||||||
|
season_loss: 0,
|
||||||
|
season_win: 0,
|
||||||
|
pool: 1
|
||||||
|
)
|
||||||
|
|
||||||
|
55.times do |i|
|
||||||
|
Match.create!(
|
||||||
|
tournament_id: @tournament.id,
|
||||||
|
weight_id: @tournament.weights.first.id,
|
||||||
|
bout_number: 910000 + i,
|
||||||
|
w1: paging_wrestler.id,
|
||||||
|
w2: @wrestlers.first.id,
|
||||||
|
bracket_position: "Pool",
|
||||||
|
round: 1
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
get :matches, params: { id: @tournament.id, search: "Pager Academy" }
|
||||||
|
assert_response :success
|
||||||
|
assert_includes response.body, "Showing 1 - 50 of 55 matches"
|
||||||
|
assert_includes response.body, "910000"
|
||||||
|
assert_not_includes response.body, "910054"
|
||||||
|
|
||||||
|
get :matches, params: { id: @tournament.id, search: "Pager Academy", page: 2 }
|
||||||
|
assert_response :success
|
||||||
|
assert_includes response.body, "Showing 51 - 55 of 55 matches"
|
||||||
|
assert_includes response.body, "910054"
|
||||||
|
assert_not_includes response.body, "910000"
|
||||||
|
end
|
||||||
|
|
||||||
test "logged in tournament owner can calculate team scores" do
|
test "logged in tournament owner can calculate team scores" do
|
||||||
sign_in_owner
|
sign_in_owner
|
||||||
post :calculate_team_scores, params: { id: 1 }
|
post :calculate_team_scores, params: { id: 1 }
|
||||||
@@ -950,6 +1252,25 @@ class TournamentsControllerTest < ActionController::TestCase
|
|||||||
assert_equal "School permission keys generated successfully.", flash[:notice]
|
assert_equal "School permission keys generated successfully.", flash[:notice]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "generated school permission keys are displayed on school delegate page" do
|
||||||
|
sign_in_owner
|
||||||
|
post :generate_school_keys, params: { id: @tournament.id }
|
||||||
|
assert_redirected_to school_delegate_path(@tournament)
|
||||||
|
|
||||||
|
@tournament.schools.reload.each do |school|
|
||||||
|
assert_not_nil school.permission_key, "Expected permission key for school #{school.id}"
|
||||||
|
assert_not_empty school.permission_key, "Expected non-empty permission key for school #{school.id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
get :school_delegate, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
|
||||||
|
@tournament.schools.each do |school|
|
||||||
|
expected_link_fragment = "/schools/#{school.id}?school_permission_key=#{school.permission_key}"
|
||||||
|
assert_includes response.body, expected_link_fragment
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "tournament delegate can delete school keys" do
|
test "tournament delegate can delete school keys" do
|
||||||
sign_in_delegate
|
sign_in_delegate
|
||||||
post :delete_school_keys, params: { id: @tournament.id }
|
post :delete_school_keys, params: { id: @tournament.id }
|
||||||
@@ -1180,4 +1501,52 @@ class TournamentsControllerTest < ActionController::TestCase
|
|||||||
expected_page2_display = [expected_page2_size, 20].min
|
expected_page2_display = [expected_page2_size, 20].min
|
||||||
assert_equal expected_page2_display, assigns(:tournaments).size, "second page should contain the remaining tournaments (or up to per_page)"
|
assert_equal expected_page2_display, assigns(:tournaments).size, "second page should contain the remaining tournaments (or up to per_page)"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "bout_sheets renders wrestler names, school names, and round for selected round" do
|
||||||
|
tournament = create_double_elim_tournament_single_weight(8, "Regular Double Elimination 1-6")
|
||||||
|
tournament.update!(user_id: users(:one).id, is_public: true)
|
||||||
|
sign_in_owner
|
||||||
|
|
||||||
|
match = tournament.matches.where.not(w1: nil, w2: nil)
|
||||||
|
.where("loser1_name != ? OR loser1_name IS NULL", "BYE")
|
||||||
|
.where("loser2_name != ? OR loser2_name IS NULL", "BYE")
|
||||||
|
.order(:bout_number)
|
||||||
|
.first
|
||||||
|
assert_not_nil match, "Expected at least one fully populated non-BYE match"
|
||||||
|
|
||||||
|
round = match.round.to_s
|
||||||
|
w1 = Wrestler.find(match.w1)
|
||||||
|
w2 = Wrestler.find(match.w2)
|
||||||
|
|
||||||
|
get :bout_sheets, params: { id: tournament.id, round: round }
|
||||||
|
assert_response :success
|
||||||
|
|
||||||
|
assert_includes response.body, "Bout Number:</strong> #{match.bout_number}"
|
||||||
|
assert_includes response.body, "Round:</strong> #{match.round}"
|
||||||
|
assert_includes response.body, "#{w1.name}-#{w1.school.name}"
|
||||||
|
assert_includes response.body, "#{w2.name}-#{w2.school.name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "bout_sheets filters out matches with BYE loser names" do
|
||||||
|
tournament = create_double_elim_tournament_single_weight(8, "Regular Double Elimination 1-6")
|
||||||
|
tournament.update!(user_id: users(:one).id, is_public: true)
|
||||||
|
sign_in_owner
|
||||||
|
|
||||||
|
bye_match = tournament.matches.order(:bout_number).first
|
||||||
|
assert_not_nil bye_match, "Expected at least one match to mark as BYE"
|
||||||
|
bye_match.update!(loser1_name: "BYE")
|
||||||
|
|
||||||
|
non_bye_match = tournament.matches.where.not(id: bye_match.id).where.not(w1: nil, w2: nil)
|
||||||
|
.where("loser1_name != ? OR loser1_name IS NULL", "BYE")
|
||||||
|
.where("loser2_name != ? OR loser2_name IS NULL", "BYE")
|
||||||
|
.order(:bout_number)
|
||||||
|
.first
|
||||||
|
assert_not_nil non_bye_match, "Expected at least one non-BYE match to remain"
|
||||||
|
|
||||||
|
get :bout_sheets, params: { id: tournament.id, round: "All" }
|
||||||
|
assert_response :success
|
||||||
|
|
||||||
|
assert_not_includes response.body, "Bout Number:</strong> #{bye_match.bout_number}"
|
||||||
|
assert_includes response.body, "Bout Number:</strong> #{non_bye_match.bout_number}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ class UpMatchesCacheTest < ActionController::TestCase
|
|||||||
mat.reload
|
mat.reload
|
||||||
movable_match = mat.queue2_match || mat.queue1_match
|
movable_match = mat.queue2_match || mat.queue1_match
|
||||||
assert movable_match, "Expected at least one queued match to move"
|
assert movable_match, "Expected at least one queued match to move"
|
||||||
mat.assign_match_to_queue!(movable_match, 4)
|
|
||||||
|
|
||||||
third_events = cache_events_for_up_matches do
|
third_events = cache_events_for_up_matches do
|
||||||
|
mat.assign_match_to_queue!(movable_match, 4)
|
||||||
get :up_matches, params: { id: @tournament.id }
|
get :up_matches, params: { id: @tournament.id }
|
||||||
assert_response :success
|
assert_response :success
|
||||||
end
|
end
|
||||||
@@ -50,13 +50,109 @@ class UpMatchesCacheTest < ActionController::TestCase
|
|||||||
assert_operator cache_writes(third_events), :>, 0, "Expected queue change to invalidate and rewrite at least one row fragment"
|
assert_operator cache_writes(third_events), :>, 0, "Expected queue change to invalidate and rewrite at least one row fragment"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "up_matches row fragments hit cache after queue clear rewrite" do
|
||||||
|
first_events = cache_events_for_up_matches do
|
||||||
|
get :up_matches, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(first_events), :>, 0, "Expected initial render to write row fragments"
|
||||||
|
|
||||||
|
mat = @tournament.mats.first
|
||||||
|
clear_events = cache_events_for_up_matches do
|
||||||
|
mat.clear_queue!
|
||||||
|
get :up_matches, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(clear_events), :>, 0, "Expected queue clear to invalidate and rewrite at least one row fragment"
|
||||||
|
|
||||||
|
repeat_events = cache_events_for_up_matches do
|
||||||
|
get :up_matches, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_equal 0, cache_writes(repeat_events), "Expected subsequent render after queue clear rewrite to reuse cached row fragments"
|
||||||
|
assert_operator cache_hits(repeat_events), :>, 0, "Expected cache hits after queue clear rewrite"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "up_matches unassigned row fragments hit cache and invalidate after unassigned match update" do
|
||||||
|
key_markers = %w[up_matches_unassigned_row]
|
||||||
|
|
||||||
|
first_events = cache_events_for_up_matches(key_markers) do
|
||||||
|
get :up_matches, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(first_events), :>, 0, "Expected initial unassigned row render to write fragments"
|
||||||
|
|
||||||
|
second_events = cache_events_for_up_matches(key_markers) do
|
||||||
|
get :up_matches, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_equal 0, cache_writes(second_events), "Expected repeat unassigned row render to reuse cached fragments"
|
||||||
|
assert_operator cache_hits(second_events), :>, 0, "Expected repeat unassigned row render to hit cache"
|
||||||
|
|
||||||
|
unassigned_match = @tournament.up_matches_unassigned_matches.first
|
||||||
|
assert unassigned_match, "Expected at least one unassigned match for cache invalidation test"
|
||||||
|
|
||||||
|
third_events = cache_events_for_up_matches(key_markers) do
|
||||||
|
unassigned_match.touch
|
||||||
|
get :up_matches, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(third_events), :>, 0, "Expected unassigned match update to invalidate unassigned row fragment"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "completing an on-mat match expires up_matches cached fragments" do
|
||||||
|
warm_events = cache_events_for_up_matches(%w[up_matches_mat_row up_matches_unassigned_row]) do
|
||||||
|
get :up_matches, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(warm_events), :>, 0, "Expected initial up_matches render to warm caches"
|
||||||
|
|
||||||
|
mat = @tournament.mats.detect { |m| m.queue1_match.present? }
|
||||||
|
assert mat, "Expected a mat with a queued match"
|
||||||
|
match = mat.queue1_match
|
||||||
|
assert match, "Expected queue1 match to complete"
|
||||||
|
|
||||||
|
post_action_events = cache_events_for_up_matches(%w[up_matches_mat_row up_matches_unassigned_row]) do
|
||||||
|
match.update!(
|
||||||
|
finished: 1,
|
||||||
|
winner_id: match.w1 || match.w2,
|
||||||
|
win_type: "Decision",
|
||||||
|
score: "1-0"
|
||||||
|
)
|
||||||
|
get :up_matches, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_operator cache_writes(post_action_events), :>, 0, "Expected completed match to expire and rewrite up_matches caches"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "manually assigning an unassigned match to a mat queue expires up_matches caches" do
|
||||||
|
warm_events = cache_events_for_up_matches(%w[up_matches_mat_row up_matches_unassigned_row]) do
|
||||||
|
get :up_matches, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(warm_events), :>, 0, "Expected initial up_matches render to warm caches"
|
||||||
|
|
||||||
|
unassigned_match = @tournament.up_matches_unassigned_matches.first
|
||||||
|
assert unassigned_match, "Expected at least one unassigned match to manually place on a mat"
|
||||||
|
target_mat = @tournament.mats.first
|
||||||
|
|
||||||
|
post_action_events = cache_events_for_up_matches(%w[up_matches_mat_row up_matches_unassigned_row]) do
|
||||||
|
target_mat.assign_match_to_queue!(unassigned_match, 4)
|
||||||
|
get :up_matches, params: { id: @tournament.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_operator cache_writes(post_action_events), :>, 0, "Expected manual mat assignment to expire and rewrite up_matches caches"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def cache_events_for_up_matches
|
def cache_events_for_up_matches(key_markers = %w[up_matches_mat_row up_matches_unassigned_row])
|
||||||
events = []
|
events = []
|
||||||
subscriber = lambda do |name, _start, _finish, _id, payload|
|
subscriber = lambda do |name, _start, _finish, _id, payload|
|
||||||
key = payload[:key].to_s
|
key = payload[:key].to_s
|
||||||
next unless key.include?("up_matches_mat_row")
|
next unless key_markers.any? { |marker| key.include?(marker) }
|
||||||
|
|
||||||
events << { name: name, hit: payload[:hit] }
|
events << { name: name, hit: payload[:hit] }
|
||||||
end
|
end
|
||||||
|
|||||||
107
test/controllers/weight_show_cache_test.rb
Normal file
107
test/controllers/weight_show_cache_test.rb
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class WeightShowCacheTest < ActionController::TestCase
|
||||||
|
tests WeightsController
|
||||||
|
|
||||||
|
setup do
|
||||||
|
create_a_tournament_with_single_weight("Regular Double Elimination 1-6", 8)
|
||||||
|
@tournament.update!(user_id: users(:one).id)
|
||||||
|
@weight = @tournament.weights.first
|
||||||
|
|
||||||
|
@original_perform_caching = ActionController::Base.perform_caching
|
||||||
|
ActionController::Base.perform_caching = true
|
||||||
|
Rails.cache.clear
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
Rails.cache.clear
|
||||||
|
ActionController::Base.perform_caching = @original_perform_caching
|
||||||
|
end
|
||||||
|
|
||||||
|
test "weight show readonly row fragments hit cache and invalidate after wrestler update" do
|
||||||
|
first_events = cache_events_for_weight_show do
|
||||||
|
get :show, params: { id: @weight.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(first_events), :>, 0, "Expected initial weight show render to write readonly row fragments"
|
||||||
|
|
||||||
|
second_events = cache_events_for_weight_show do
|
||||||
|
get :show, params: { id: @weight.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_equal 0, cache_writes(second_events), "Expected repeat weight show render to reuse readonly row fragments"
|
||||||
|
assert_operator cache_hits(second_events), :>, 0, "Expected repeat weight show render to hit readonly row cache"
|
||||||
|
|
||||||
|
wrestler = @weight.wrestlers.first
|
||||||
|
third_events = cache_events_for_weight_show do
|
||||||
|
wrestler.touch
|
||||||
|
get :show, params: { id: @weight.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_writes(third_events), :>, 0, "Expected wrestler update to invalidate weight show readonly row cache"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "weight show does not leak manage-only controls from cache across users" do
|
||||||
|
sign_in users(:one)
|
||||||
|
get :show, params: { id: @weight.id }
|
||||||
|
assert_response :success
|
||||||
|
assert_includes response.body, "Save Seeds"
|
||||||
|
assert_match(/fa-trash-alt/, response.body)
|
||||||
|
assert_match(/name="wrestler\[\d+\]\[original_seed\]"/, response.body)
|
||||||
|
|
||||||
|
sign_out
|
||||||
|
|
||||||
|
get :show, params: { id: @weight.id }
|
||||||
|
assert_response :success
|
||||||
|
assert_not_includes response.body, "Save Seeds"
|
||||||
|
assert_no_match(/fa-trash-alt/, response.body)
|
||||||
|
assert_no_match(/name="wrestler\[\d+\]\[original_seed\]"/, response.body)
|
||||||
|
|
||||||
|
spectator_cache_events = cache_events_for_weight_show do
|
||||||
|
get :show, params: { id: @weight.id }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
assert_operator cache_hits(spectator_cache_events), :>, 0, "Expected repeat spectator request to hit readonly wrestler row cache"
|
||||||
|
assert_not_includes response.body, "Save Seeds"
|
||||||
|
assert_no_match(/fa-trash-alt/, response.body)
|
||||||
|
assert_no_match(/name="wrestler\[\d+\]\[original_seed\]"/, response.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def sign_out
|
||||||
|
@request.session[:user_id] = nil
|
||||||
|
@controller.instance_variable_set(:@current_user, nil)
|
||||||
|
@controller.instance_variable_set(:@current_ability, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_events_for_weight_show
|
||||||
|
events = []
|
||||||
|
subscriber = lambda do |name, _start, _finish, _id, payload|
|
||||||
|
key = payload[:key].to_s
|
||||||
|
next unless key.include?("weight_show_wrestler_row")
|
||||||
|
|
||||||
|
events << { name: name, hit: payload[:hit] }
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveSupport::Notifications.subscribed(
|
||||||
|
subscriber,
|
||||||
|
/cache_(read|write|fetch_hit|generate)\.active_support/
|
||||||
|
) do
|
||||||
|
yield
|
||||||
|
end
|
||||||
|
|
||||||
|
events
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_writes(events)
|
||||||
|
events.count { |event| event[:name] == "cache_write.active_support" }
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_hits(events)
|
||||||
|
events.count do |event|
|
||||||
|
event[:name] == "cache_fetch_hit.active_support" ||
|
||||||
|
(event[:name] == "cache_read.active_support" && event[:hit])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -30,6 +30,15 @@ class DoubleEliminationWrestlerScore < ActionDispatch::IntegrationTest
|
|||||||
return wrestler
|
return wrestler
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def wrestle_other_match_in_round(reference_match, conso: false)
|
||||||
|
match = @tournament.matches.reload
|
||||||
|
.select { |m| m.round == reference_match.round && m.id != reference_match.id && m.is_consolation_match == conso }
|
||||||
|
.first
|
||||||
|
return if match.nil?
|
||||||
|
|
||||||
|
winner_by_name("Test2", match)
|
||||||
|
end
|
||||||
|
|
||||||
test "Wrestlers get points for byes in the championship rounds" do
|
test "Wrestlers get points for byes in the championship rounds" do
|
||||||
matches = @tournament.matches.reload
|
matches = @tournament.matches.reload
|
||||||
round1 = matches.select{|m| m.bracket_position == "Bracket Round of 16"}.first
|
round1 = matches.select{|m| m.bracket_position == "Bracket Round of 16"}.first
|
||||||
@@ -37,9 +46,10 @@ class DoubleEliminationWrestlerScore < ActionDispatch::IntegrationTest
|
|||||||
semi = matches.select{|m| m.bracket_position == "Semis"}.first
|
semi = matches.select{|m| m.bracket_position == "Semis"}.first
|
||||||
winner_by_name_by_bye("Test1", round1)
|
winner_by_name_by_bye("Test1", round1)
|
||||||
winner_by_name_by_bye("Test1", quarter)
|
winner_by_name_by_bye("Test1", quarter)
|
||||||
|
wrestle_other_match_in_round(round1, conso: false)
|
||||||
winner_by_name("Test1", semi)
|
winner_by_name("Test1", semi)
|
||||||
wrestler_points_calc = CalculateWrestlerTeamScore.new(get_wretler_by_name("Test1"))
|
wrestler_points_calc = CalculateWrestlerTeamScore.new(get_wretler_by_name("Test1"))
|
||||||
assert wrestler_points_calc.byePoints == 4
|
assert wrestler_points_calc.byePoints == 2
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Wrestlers get points for byes in the consolation rounds" do
|
test "Wrestlers get points for byes in the consolation rounds" do
|
||||||
@@ -49,9 +59,10 @@ class DoubleEliminationWrestlerScore < ActionDispatch::IntegrationTest
|
|||||||
semi = matches.select{|m| m.bracket_position == "Conso Semis"}.first
|
semi = matches.select{|m| m.bracket_position == "Conso Semis"}.first
|
||||||
winner_by_name_by_bye("Test1", conso_r8_1)
|
winner_by_name_by_bye("Test1", conso_r8_1)
|
||||||
winner_by_name_by_bye("Test1", quarter)
|
winner_by_name_by_bye("Test1", quarter)
|
||||||
|
wrestle_other_match_in_round(conso_r8_1, conso: true)
|
||||||
winner_by_name("Test1", semi)
|
winner_by_name("Test1", semi)
|
||||||
wrestler_points_calc = CalculateWrestlerTeamScore.new(get_wretler_by_name("Test1"))
|
wrestler_points_calc = CalculateWrestlerTeamScore.new(get_wretler_by_name("Test1"))
|
||||||
assert wrestler_points_calc.byePoints == 2
|
assert wrestler_points_calc.byePoints == 1
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Wrestlers do not get bye points if they get byes to 1st/2nd and win by bye" do
|
test "Wrestlers do not get bye points if they get byes to 1st/2nd and win by bye" do
|
||||||
@@ -93,7 +104,19 @@ class DoubleEliminationWrestlerScore < ActionDispatch::IntegrationTest
|
|||||||
winner_by_name_by_bye("Test1", semi)
|
winner_by_name_by_bye("Test1", semi)
|
||||||
winner_by_name("Test1", final)
|
winner_by_name("Test1", final)
|
||||||
wrestler_points_calc = CalculateWrestlerTeamScore.new(get_wretler_by_name("Test1"))
|
wrestler_points_calc = CalculateWrestlerTeamScore.new(get_wretler_by_name("Test1"))
|
||||||
assert wrestler_points_calc.byePoints == 6
|
assert wrestler_points_calc.byePoints == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "Wrestlers do not get championship bye points when no championship match is wrestled in those bye rounds" do
|
||||||
|
matches = @tournament.matches.reload
|
||||||
|
round1 = matches.select{|m| m.bracket_position == "Bracket Round of 16"}.first
|
||||||
|
quarter = matches.select{|m| m.bracket_position == "Quarter"}.first
|
||||||
|
semi = matches.select{|m| m.bracket_position == "Semis"}.first
|
||||||
|
winner_by_name_by_bye("Test1", round1)
|
||||||
|
winner_by_name_by_bye("Test1", quarter)
|
||||||
|
winner_by_name("Test1", semi)
|
||||||
|
wrestler_points_calc = CalculateWrestlerTeamScore.new(get_wretler_by_name("Test1"))
|
||||||
|
assert wrestler_points_calc.byePoints == 0
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Wrestlers do not get bye points if they get byes to 3rd/4th and win by decision" do
|
test "Wrestlers do not get bye points if they get byes to 3rd/4th and win by decision" do
|
||||||
@@ -107,6 +130,20 @@ class DoubleEliminationWrestlerScore < ActionDispatch::IntegrationTest
|
|||||||
winner_by_name_by_bye("Test1", semi)
|
winner_by_name_by_bye("Test1", semi)
|
||||||
winner_by_name("Test1", final)
|
winner_by_name("Test1", final)
|
||||||
wrestler_points_calc = CalculateWrestlerTeamScore.new(get_wretler_by_name("Test1"))
|
wrestler_points_calc = CalculateWrestlerTeamScore.new(get_wretler_by_name("Test1"))
|
||||||
assert wrestler_points_calc.byePoints == 3
|
assert wrestler_points_calc.byePoints == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "Wrestlers do not get conso bye points when no conso match is wrestled in those rounds" do
|
||||||
|
matches = @tournament.matches.reload
|
||||||
|
conso_r8_1 = matches.select{|m| m.bracket_position == "Conso Round of 8.1"}.first
|
||||||
|
quarter = matches.select{|m| m.bracket_position == "Conso Quarter"}.first
|
||||||
|
semi = matches.select{|m| m.bracket_position == "Conso Semis"}.first
|
||||||
|
final = matches.select{|m| m.bracket_position == "3/4"}.first
|
||||||
|
winner_by_name_by_bye("Test1", conso_r8_1)
|
||||||
|
winner_by_name_by_bye("Test1", quarter)
|
||||||
|
winner_by_name_by_bye("Test1", semi)
|
||||||
|
winner_by_name("Test1", final)
|
||||||
|
wrestler_points_calc = CalculateWrestlerTeamScore.new(get_wretler_by_name("Test1"))
|
||||||
|
assert wrestler_points_calc.byePoints == 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user