Add skin value and back-nine doubling to skins games
Players can now set a monetary value per skin when starting a skins game. For 18-hole games, an option to double the value on holes 10–18 is available. The value is stored at game creation and displayed throughout the live game and summary (leaderboard, scorecard, and totals). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -241,12 +241,12 @@
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="totals-row">
|
||||
<td>Skins</td>
|
||||
<td x-text="SKIN_VALUE ? 'Value' : 'Skins'"></td>
|
||||
{% if hole_pars %}<td></td>{% endif %}
|
||||
{% for p in players %}
|
||||
<td x-text="calcSkins().skins['{{ p.id }}'] || 0"></td>
|
||||
<td x-text="SKIN_VALUE ? (parseFloat(calcSkins().money['{{ p.id }}'].toPrecision(4)) || 0) : (calcSkins().skins['{{ p.id }}'] || 0)"></td>
|
||||
{% endfor %}
|
||||
<td x-text="calcSkins().unclaimed ? '(' + calcSkins().unclaimed + ')' : ''"></td>
|
||||
<td x-text="SKIN_VALUE ? (calcSkins().unclaimedMoney ? '(' + parseFloat(calcSkins().unclaimedMoney.toPrecision(4)) + ')' : '') : (calcSkins().unclaimed ? '(' + calcSkins().unclaimed + ')' : '')"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
@@ -278,6 +278,8 @@ const GAME_ID = "{{ game.id }}";
|
||||
const GAME_HOLES = {{ game.holes_count }};
|
||||
const GAME_FORMAT = "{{ game.format }}";
|
||||
const SKINS_CARRYOVER = {{ carryover | tojson }};
|
||||
const SKIN_VALUE = {{ skin_value | tojson }};
|
||||
const SKIN_DOUBLE_BACK9 = {{ skin_double_back9 | tojson }};
|
||||
const HOLE_PARS = {{ hole_pars | tojson }};
|
||||
const HOLE_SIS = {{ hole_stroke_indices | tojson }};
|
||||
const STROKES_PER_HOLE = {{ strokes_per_hole | tojson }};
|
||||
@@ -483,9 +485,11 @@ function gameApp() {
|
||||
|
||||
calcSkins() {
|
||||
const skins = {};
|
||||
PLAYERS_DATA.forEach(p => skins[p.id] = 0);
|
||||
const money = {};
|
||||
PLAYERS_DATA.forEach(p => { skins[p.id] = 0; money[p.id] = 0; });
|
||||
const holes = {};
|
||||
let pot = 0;
|
||||
let moneyPot = 0;
|
||||
for (let hole = 1; hole <= GAME_HOLES; hole++) {
|
||||
if (this.isSkipped(hole)) continue;
|
||||
const holeScores = this.scores[hole] || {};
|
||||
@@ -494,18 +498,21 @@ function gameApp() {
|
||||
.filter(x => x.strokes > 0);
|
||||
if (scored.length === 0) continue;
|
||||
pot += 1;
|
||||
if (SKIN_VALUE) moneyPot += SKIN_VALUE * (SKIN_DOUBLE_BACK9 && hole >= 10 ? 2 : 1);
|
||||
const min = Math.min(...scored.map(x => x.strokes));
|
||||
const winners = scored.filter(x => x.strokes === min);
|
||||
if (winners.length === 1) {
|
||||
skins[winners[0].id] += pot;
|
||||
holes[hole] = { winner: winners[0].id, pot };
|
||||
money[winners[0].id] += moneyPot;
|
||||
holes[hole] = { winner: winners[0].id, pot, moneyPot };
|
||||
pot = 0;
|
||||
moneyPot = 0;
|
||||
} else {
|
||||
holes[hole] = { winner: null, pot };
|
||||
if (!SKINS_CARRYOVER) pot = 0;
|
||||
holes[hole] = { winner: null, pot, moneyPot };
|
||||
if (!SKINS_CARRYOVER) { pot = 0; moneyPot = 0; }
|
||||
}
|
||||
}
|
||||
return { skins, holes, unclaimed: pot };
|
||||
return { skins, money, holes, unclaimed: pot, unclaimedMoney: moneyPot };
|
||||
},
|
||||
|
||||
isSkinWinner(hole, uid) {
|
||||
@@ -515,6 +522,11 @@ function gameApp() {
|
||||
skinHoleLabel(hole) {
|
||||
const h = this.calcSkins().holes[hole];
|
||||
if (!h) return '';
|
||||
if (SKIN_VALUE) {
|
||||
const v = parseFloat(h.moneyPot.toPrecision(4));
|
||||
if (h.winner) return '' + v;
|
||||
return '→ ' + v;
|
||||
}
|
||||
if (h.winner) return h.pot > 1 ? '✓ ' + h.pot : '✓';
|
||||
return '→ ' + h.pot;
|
||||
},
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
courseSearch: '',
|
||||
format: 'bingo_bango_bongo',
|
||||
carryoverEnabled: true,
|
||||
skinValue: '',
|
||||
skinDoubleBackNine: false,
|
||||
holesCount: '',
|
||||
selectedPlayers: ['{{ user.id }}'],
|
||||
selectedTees: {},
|
||||
@@ -62,10 +64,10 @@
|
||||
const player = this.getPlayer(uid);
|
||||
const tee = this.getTee(uid);
|
||||
const course = COURSES_DATA.find(c => c.id === this.courseId);
|
||||
if (!course || !tee || player.handicap == null) return null;
|
||||
const coursePar = (parseInt(this.holesCount) === 9 && course.holes === 18) ? course.par_9 : course.par;
|
||||
if (!coursePar) return null;
|
||||
return Math.round(player.handicap * (tee.slope / 113) + (tee.rating - coursePar));
|
||||
if (!course || !tee || player.handicap == null || !course.par) return null;
|
||||
const nineOfEighteen = parseInt(this.holesCount) === 9 && course.holes === 18;
|
||||
const ph = Math.round(player.handicap * (tee.slope / 113) + (tee.rating - course.par));
|
||||
return nineOfEighteen ? Math.round(ph / 2) : ph;
|
||||
},
|
||||
goToConfirm() { this.step = 3; },
|
||||
backFromConfirm() { this.step = this.hasTees() ? 2 : 1; },
|
||||
@@ -101,12 +103,25 @@
|
||||
<span>Skins</span>
|
||||
</label>
|
||||
</div>
|
||||
<div x-show="format === 'skins'" style="margin-top:0.75rem;">
|
||||
<div x-show="format === 'skins'" style="margin-top:0.75rem; display:flex; flex-direction:column; gap:0.5rem;">
|
||||
<label style="display:flex; align-items:center; gap:0.5rem; cursor:pointer;">
|
||||
<input type="checkbox" name="carryover" value="1" checked style="width:auto;"
|
||||
@change="carryoverEnabled = $event.target.checked">
|
||||
<span style="font-weight:400; color:var(--text-muted);">Carryover (ties carry to next hole)</span>
|
||||
</label>
|
||||
<div style="display:flex; align-items:center; gap:0.75rem;">
|
||||
<span style="font-weight:400; color:var(--text-muted); font-size:0.95rem;">Value per skin</span>
|
||||
<input type="number" name="skin_value" x-model="skinValue" min="0" step="0.5"
|
||||
placeholder="—"
|
||||
style="width:5rem; padding:0.4rem 0.5rem; border-radius:0.4rem; border:1px solid var(--border); background:var(--surface2); color:var(--text); font-size:1rem; text-align:center;">
|
||||
</div>
|
||||
<div x-show="skinValue && holesCount == 18">
|
||||
<label style="display:flex; align-items:center; gap:0.5rem; cursor:pointer;">
|
||||
<input type="checkbox" name="skin_double_back_nine" value="1" style="width:auto;"
|
||||
@change="skinDoubleBackNine = $event.target.checked">
|
||||
<span style="font-weight:400; color:var(--text-muted);">Double value on back nine (holes 10–18)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -257,6 +272,10 @@
|
||||
<span class="confirm-label">Carryover</span>
|
||||
<span class="confirm-value" x-text="carryoverEnabled ? 'Yes' : 'No'"></span>
|
||||
</div>
|
||||
<div class="confirm-row" x-show="format === 'skins' && skinValue">
|
||||
<span class="confirm-label">Skin value</span>
|
||||
<span class="confirm-value" x-text="skinValue + (skinDoubleBackNine ? ' · doubles on back 9' : '')"></span>
|
||||
</div>
|
||||
<div class="confirm-row">
|
||||
<span class="confirm-label">Course</span>
|
||||
<span class="confirm-value" x-text="courseLabel || '—'"></span>
|
||||
|
||||
@@ -16,8 +16,13 @@
|
||||
<div class="card" style="padding:0.75rem 1rem;">
|
||||
{% for p in results %}
|
||||
{% if game.format == 'skins' %}
|
||||
{% set score_val = skins_won.get(p.id, 0) %}
|
||||
{% set score_label = score_val | string + ' skin' + ('s' if score_val != 1 else '') %}
|
||||
{% if skin_value and money_won %}
|
||||
{% set score_val = money_won.get(p.id, 0) %}
|
||||
{% set score_label = "%.4g" % score_val %}
|
||||
{% else %}
|
||||
{% set score_val = skins_won.get(p.id, 0) %}
|
||||
{% set score_label = score_val | string + ' skin' + ('s' if score_val != 1 else '') %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set t = totals.get(p.id, {}) %}
|
||||
{% set score_val = t.total %}
|
||||
@@ -43,7 +48,8 @@
|
||||
{% endfor %}
|
||||
{% if game.format == 'skins' and unclaimed %}
|
||||
<div style="font-size:0.8rem; color:var(--text-muted); margin-top:0.5rem; text-align:center;">
|
||||
{{ unclaimed }} skin{{ 's' if unclaimed != 1 else '' }} unclaimed
|
||||
{% if skin_value and unclaimed_money %}{{ "%.4g" % unclaimed_money }} unclaimed
|
||||
{% else %}{{ unclaimed }} skin{{ 's' if unclaimed != 1 else '' }} unclaimed{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -246,8 +252,12 @@
|
||||
{% endfor %}
|
||||
<td class="score-cell">
|
||||
{% if hr %}
|
||||
{% if hr.winner %}✓{% if hr.pot > 1 %} {{ hr.pot }}{% endif %}
|
||||
{% else %}→ {{ hr.pot }}{% endif %}
|
||||
{% if skin_value %}
|
||||
{% if hr.winner %}{{ "%.4g" % hr.money_pot }}{% else %}→ {{ "%.4g" % hr.money_pot }}{% endif %}
|
||||
{% else %}
|
||||
{% if hr.winner %}✓{% if hr.pot > 1 %} {{ hr.pot }}{% endif %}
|
||||
{% else %}→ {{ hr.pot }}{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -255,12 +265,16 @@
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="totals-row">
|
||||
<td>Skins</td>
|
||||
<td>{% if skin_value %}Value{% else %}Skins{% endif %}</td>
|
||||
{% if hole_pars %}<td></td>{% endif %}
|
||||
{% for p in players %}
|
||||
{% if skin_value and money_won %}
|
||||
<td>{{ "%.4g" % money_won.get(p.id, 0) }}</td>
|
||||
{% else %}
|
||||
<td>{{ skins_won.get(p.id, 0) }}</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<td>{% if unclaimed %}({{ unclaimed }}){% endif %}</td>
|
||||
<td>{% if skin_value and unclaimed_money %}({{ "%.4g" % unclaimed_money }}){% elif unclaimed %}({{ unclaimed }}){% endif %}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user