feat: implement variable token offers and auto-round advancement
- Add variable offer system where P1 can offer any amount of tokens - Players start with 10 tokens each (P1: pavos, P2: elotes) - Implement offer/request mechanism with token validation - Auto-advance rounds after P2 actions or P1 no-offer - G2: Force offer by default, disable no-offer button when forced - G3: Wait for shame decision after snatch before advancing - G4: Implement inverse sanction (P1 gets requested without giving offered) - Reset rounds to 1 when changing game variants - Fix OfferControls responsiveness issues - Hide offer controls after active offer - Update all G1-G5 components with proper offer flow
This commit is contained in:
@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router';
|
|||||||
import Lobby from '../views/Lobby.vue';
|
import Lobby from '../views/Lobby.vue';
|
||||||
import Game from '../views/Game.vue';
|
import Game from '../views/Game.vue';
|
||||||
import Dashboard from '../views/Dashboard.vue';
|
import Dashboard from '../views/Dashboard.vue';
|
||||||
|
import DemoGame from '../views/DemoGame.vue';
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
@@ -16,6 +17,11 @@ const router = createRouter({
|
|||||||
name: 'Game',
|
name: 'Game',
|
||||||
component: Game
|
component: Game
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/demo',
|
||||||
|
name: 'DemoGame',
|
||||||
|
component: DemoGame
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
|
|||||||
@@ -145,6 +145,55 @@ class ColyseusService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Demo game helpers
|
||||||
|
setVariant(variant: string): void {
|
||||||
|
if (this.gameRoom.value) {
|
||||||
|
this.gameRoom.value.send("setVariant", variant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p2Force(force: boolean): void {
|
||||||
|
if (this.gameRoom.value) {
|
||||||
|
this.gameRoom.value.send("p2Force", force);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p1Action(action: 'offer' | 'no_offer' | 'forced_offer'): void {
|
||||||
|
if (this.gameRoom.value) {
|
||||||
|
this.gameRoom.value.send("p1Action", action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p2Action(action: 'accept' | 'reject' | 'snatch'): void {
|
||||||
|
if (this.gameRoom.value) {
|
||||||
|
this.gameRoom.value.send("p2Action", action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
report(report: boolean): void {
|
||||||
|
if (this.gameRoom.value) {
|
||||||
|
this.gameRoom.value.send("report", report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assignShame(assign: boolean): void {
|
||||||
|
if (this.gameRoom.value) {
|
||||||
|
this.gameRoom.value.send("assignShame", assign);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proposeOffer(offerPavo: number, offerElote: number, requestPavo: number, requestElote: number): void {
|
||||||
|
if (this.gameRoom.value) {
|
||||||
|
this.gameRoom.value.send("proposeOffer", { offerPavo, offerElote, requestPavo, requestElote });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
noOffer(): void {
|
||||||
|
if (this.gameRoom.value) {
|
||||||
|
this.gameRoom.value.send("noOffer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
leaveLobby(): void {
|
leaveLobby(): void {
|
||||||
console.log('leaveLobby called');
|
console.log('leaveLobby called');
|
||||||
if (this.lobbyRoom.value) {
|
if (this.lobbyRoom.value) {
|
||||||
|
|||||||
219
client/src/views/DemoGame.vue
Normal file
219
client/src/views/DemoGame.vue
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
<template>
|
||||||
|
<div class="game">
|
||||||
|
<div class="game-container">
|
||||||
|
<div class="game-header">
|
||||||
|
<h1>🧪 Demo Room</h1>
|
||||||
|
<div class="meta">
|
||||||
|
<div>Room: <code>{{ roomId }}</code></div>
|
||||||
|
<div>Round: {{ currentRound }}/3</div>
|
||||||
|
<div>Status: <span class="badge">{{ gameStatus }}</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="variant-selector">
|
||||||
|
<button v-for="g in variants" :key="g" @click="setVariant(g)" :class="['btn', 'btn-variant', { active: currentVariant === g }]">
|
||||||
|
{{ g }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="players-section">
|
||||||
|
<div v-for="p in players" :key="p.sessionId" class="player-card" :class="{ 'current-player': p.sessionId === sessionId }">
|
||||||
|
<div class="player-name">{{ p.name }}</div>
|
||||||
|
<div class="player-role">Role: {{ p.role || '—' }}</div>
|
||||||
|
<div class="player-tokens">
|
||||||
|
<span>🦃 {{ p.pavoTokens }}</span>
|
||||||
|
<span>🌽 {{ p.eloteTokens }}</span>
|
||||||
|
<span v-if="p.shameTokens">😶 {{ p.shameTokens }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="gameStatus === 'waiting'" class="waiting-area">
|
||||||
|
<div class="waiting-message">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<h2>Waiting for opponent...</h2>
|
||||||
|
<p>Players in room: {{ players.length }}/2</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="gameplay">
|
||||||
|
<component :is="currentComponent"
|
||||||
|
:state="roundState"
|
||||||
|
:my-role="myRole"
|
||||||
|
@p2Force="onP2Force"
|
||||||
|
@p1Action="onP1Action"
|
||||||
|
@p2Action="onP2Action"
|
||||||
|
@report="onReport"
|
||||||
|
@assignShame="onAssignShame"
|
||||||
|
@proposeOffer="onProposeOffer"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="outcome" v-if="outcomeP1 || outcomeP2">
|
||||||
|
<div class="outcome-box">
|
||||||
|
<div>Outcome P1: <strong>{{ outcomeP1 }}</strong></div>
|
||||||
|
<div>Outcome P2: <strong>{{ outcomeP2 }}</strong></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="game-footer">
|
||||||
|
<button @click="leaveGame" class="btn btn-leave">Leave Game</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { colyseusService } from '../services/colyseus';
|
||||||
|
import { getStateCallbacks } from 'colyseus.js';
|
||||||
|
|
||||||
|
import G1 from './games/G1.vue';
|
||||||
|
import G2 from './games/G2.vue';
|
||||||
|
import G3 from './games/G3.vue';
|
||||||
|
import G4 from './games/G4.vue';
|
||||||
|
import G5 from './games/G5.vue';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const players = ref<any[]>([]);
|
||||||
|
const gameStatus = ref('waiting');
|
||||||
|
const roomId = ref('');
|
||||||
|
const currentVariant = ref<'G1'|'G2'|'G3'|'G4'|'G5'>('G1');
|
||||||
|
const currentRound = ref(1);
|
||||||
|
const p1Action = ref('');
|
||||||
|
const p2Action = ref('');
|
||||||
|
const forcedByP2 = ref(false);
|
||||||
|
const reported = ref(false);
|
||||||
|
const shameAssigned = ref(false);
|
||||||
|
const outcomeP1 = ref(0);
|
||||||
|
const outcomeP2 = ref(0);
|
||||||
|
|
||||||
|
const variants = ['G1','G2','G3','G4','G5'];
|
||||||
|
|
||||||
|
const sessionId = computed(() => colyseusService.sessionId.value);
|
||||||
|
const myRole = computed(() => {
|
||||||
|
const me = players.value.find(p => p.sessionId === sessionId.value);
|
||||||
|
return me?.role || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const roundState = computed(() => ({
|
||||||
|
currentVariant: currentVariant.value,
|
||||||
|
currentRound: currentRound.value,
|
||||||
|
p1Action: p1Action.value,
|
||||||
|
p2Action: p2Action.value,
|
||||||
|
forcedByP2: forcedByP2.value,
|
||||||
|
reported: reported.value,
|
||||||
|
shameAssigned: shameAssigned.value,
|
||||||
|
offer: {
|
||||||
|
offerPavo: roomOffer('offerPavo'),
|
||||||
|
offerElote: roomOffer('offerElote'),
|
||||||
|
requestPavo: roomOffer('requestPavo'),
|
||||||
|
requestElote: roomOffer('requestElote'),
|
||||||
|
active: roomOffer('offerActive')
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const componentMap: Record<string, any> = { G1, G2, G3, G4, G5 };
|
||||||
|
const currentComponent = computed(() => componentMap[currentVariant.value]);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const room = colyseusService.gameRoom.value;
|
||||||
|
if (!room) {
|
||||||
|
router.push('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $ = getStateCallbacks(room);
|
||||||
|
|
||||||
|
room.onStateChange.once((state: any) => {
|
||||||
|
gameStatus.value = state.gameStatus || 'waiting';
|
||||||
|
});
|
||||||
|
|
||||||
|
$(room.state).listen("gameStatus", (value: string) => { gameStatus.value = value; });
|
||||||
|
$(room.state).listen("roomId", (value: string) => { roomId.value = value; });
|
||||||
|
$(room.state).listen("currentVariant", (value: string) => { currentVariant.value = value as any; });
|
||||||
|
$(room.state).listen("currentRound", (value: number) => { currentRound.value = value; });
|
||||||
|
$(room.state).listen("p1Action", (value: string) => { p1Action.value = value; });
|
||||||
|
$(room.state).listen("p2Action", (value: string) => { p2Action.value = value; });
|
||||||
|
$(room.state).listen("forcedByP2", (value: boolean) => { forcedByP2.value = value; });
|
||||||
|
$(room.state).listen("reported", (value: boolean) => { reported.value = value; });
|
||||||
|
$(room.state).listen("shameAssigned", (value: boolean) => { shameAssigned.value = value; });
|
||||||
|
// Offer fields
|
||||||
|
$(room.state).listen("offerPavo", () => forceUpdate());
|
||||||
|
$(room.state).listen("offerElote", () => forceUpdate());
|
||||||
|
$(room.state).listen("requestPavo", () => forceUpdate());
|
||||||
|
$(room.state).listen("requestElote", () => forceUpdate());
|
||||||
|
$(room.state).listen("offerActive", () => forceUpdate());
|
||||||
|
|
||||||
|
$(room.state).players.onAdd((player: any, key: string) => {
|
||||||
|
const idx = players.value.findIndex(p => p.sessionId === key);
|
||||||
|
if (idx === -1) {
|
||||||
|
players.value.push({
|
||||||
|
sessionId: key,
|
||||||
|
name: player.name,
|
||||||
|
role: player.role,
|
||||||
|
pavoTokens: player.pavoTokens,
|
||||||
|
eloteTokens: player.eloteTokens,
|
||||||
|
shameTokens: player.shameTokens,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$(player).listen("role", (v: string) => { const p = players.value.find(x => x.sessionId === key); if (p) p.role = v; });
|
||||||
|
$(player).listen("pavoTokens", (v: number) => { const p = players.value.find(x => x.sessionId === key); if (p) p.pavoTokens = v; });
|
||||||
|
$(player).listen("eloteTokens", (v: number) => { const p = players.value.find(x => x.sessionId === key); if (p) p.eloteTokens = v; });
|
||||||
|
$(player).listen("shameTokens", (v: number) => { const p = players.value.find(x => x.sessionId === key); if (p) p.shameTokens = v; });
|
||||||
|
});
|
||||||
|
$(room.state).players.onRemove((player: any, key: string) => {
|
||||||
|
const i = players.value.findIndex(p => p.sessionId === key);
|
||||||
|
if (i !== -1) players.value.splice(i, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
room.onMessage("playerInfo", (info: any) => {
|
||||||
|
colyseusService.sessionId.value = info.sessionId;
|
||||||
|
colyseusService.playerName.value = info.name;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function roomOffer<K extends string>(key: K): any {
|
||||||
|
const room = colyseusService.gameRoom.value as any;
|
||||||
|
return room?.state?.[key as any];
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshTick = ref(0);
|
||||||
|
function forceUpdate() { refreshTick.value++; }
|
||||||
|
|
||||||
|
function setVariant(g: string) { colyseusService.setVariant(g); }
|
||||||
|
function onP2Force(force: boolean) { colyseusService.p2Force(force); }
|
||||||
|
function onP1Action(action: 'no_offer') { colyseusService.noOffer(); }
|
||||||
|
function onProposeOffer(payload: { offerPavo:number; offerElote:number; requestPavo:number; requestElote:number; }) { colyseusService.proposeOffer(payload.offerPavo, payload.offerElote, payload.requestPavo, payload.requestElote); }
|
||||||
|
function onP2Action(action: 'accept'|'reject'|'snatch') { colyseusService.p2Action(action); }
|
||||||
|
function onReport(val: boolean) { colyseusService.report(val); }
|
||||||
|
function onAssignShame(val: boolean) { colyseusService.assignShame(val); }
|
||||||
|
|
||||||
|
function leaveGame() { colyseusService.leaveGame();
|
||||||
|
router.push('/'); }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.game { min-height: 100vh; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display:flex; align-items:center; justify-content:center; padding:20px; }
|
||||||
|
.game-container { background: white; border-radius: 20px; padding: 24px; max-width: 1000px; width: 100%; box-shadow: 0 20px 60px rgba(0,0,0,0.3); }
|
||||||
|
.game-header { display:flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
|
||||||
|
.game-header h1 { margin: 0; font-size: 20px; }
|
||||||
|
.meta { display:flex; gap: 16px; font-size: 14px; }
|
||||||
|
.badge { background:#e3f2fd; color:#2196f3; padding: 2px 8px; border-radius: 12px; font-size: 12px; }
|
||||||
|
.variant-selector { display:flex; gap: 8px; }
|
||||||
|
.btn { padding: 8px 12px; border-radius: 8px; border: none; cursor: pointer; }
|
||||||
|
.btn-variant { background: #f2f2f2; }
|
||||||
|
.btn-variant.active { background: #667eea; color: white; }
|
||||||
|
.btn-next { background:#2196f3; color:white; margin-top: 12px; }
|
||||||
|
.btn-leave { background:#f44336; color:white; }
|
||||||
|
.players-section { display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin: 12px 0; }
|
||||||
|
.player-card { padding: 12px; background:#f8f9fa; border-radius: 10px; }
|
||||||
|
.player-card.current-player { outline: 2px solid #667eea; }
|
||||||
|
.player-role { color:#666; margin-top: 4px; }
|
||||||
|
.player-tokens { display:flex; gap: 12px; margin-top: 8px; }
|
||||||
|
.waiting-area { text-align:center; padding: 24px 0; }
|
||||||
|
.spinner { width:40px; height:40px; border: 4px solid #eee; border-top:4px solid #667eea; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 8px; }
|
||||||
|
@keyframes spin { 0%{transform:rotate(0)} 100%{transform:rotate(360deg)} }
|
||||||
|
.outcome-box { display:flex; gap: 24px; background:#f5f5f5; padding: 12px; border-radius: 8px; }
|
||||||
|
</style>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
<div class="main-actions">
|
<div class="main-actions">
|
||||||
<button @click="handleQuickPlay" class="btn btn-primary btn-large" :disabled="isJoining">
|
<button @click="handleQuickPlay" class="btn btn-primary btn-large" :disabled="isJoining">
|
||||||
<span v-if="!isJoining">⚡ Quick Play</span>
|
<span v-if="!isJoining">🧪 Demo Play</span>
|
||||||
<span v-else>Finding match...</span>
|
<span v-else>Finding match...</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -156,8 +156,8 @@ async function handleQuickPlay() {
|
|||||||
colyseusService.lobbyRoom.value = null;
|
colyseusService.lobbyRoom.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Navigating to /game...');
|
console.log('Navigating to /demo...');
|
||||||
await router.push('/game');
|
await router.push('/demo');
|
||||||
console.log('Navigation complete');
|
console.log('Navigation complete');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to join game:', error);
|
console.error('Failed to join game:', error);
|
||||||
|
|||||||
35
client/src/views/games/G1.vue
Normal file
35
client/src/views/games/G1.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div class="g">
|
||||||
|
<h3>G1 – Sin derechos de propiedad</h3>
|
||||||
|
<OfferControls v-if="myRole==='P1' && !state.offer?.active" @propose="onPropose" @no-offer="onNoOffer"/>
|
||||||
|
<div v-if="state.offer?.active" class="controls">
|
||||||
|
<div class="offer-view">Oferta: 🦃 {{ state.offer.offerPavo }} / 🌽 {{ state.offer.offerElote }} | Pedido: 🦃 {{ state.offer.requestPavo }} / 🌽 {{ state.offer.requestElote }}</div>
|
||||||
|
<div v-if="myRole === 'P2'">
|
||||||
|
<button class="btn" @click="$emit('p2Action', 'accept')">P2: Aceptar</button>
|
||||||
|
<button class="btn" @click="$emit('p2Action', 'reject')">P2: Rechazar</button>
|
||||||
|
<button class="btn" @click="$emit('p2Action', 'snatch')">P2: Robar</button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="hint">Esperando decisión de P2…</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import OfferControls from './OfferControls.vue';
|
||||||
|
const props = defineProps<{ state: any; myRole: string }>();
|
||||||
|
const emit = defineEmits(['p1Action','p2Action','proposeOffer']);
|
||||||
|
function onPropose(payload: any) {
|
||||||
|
emit('proposeOffer', payload);
|
||||||
|
}
|
||||||
|
function onNoOffer() {
|
||||||
|
emit('p1Action', 'no_offer');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.g { background:#fff; padding:12px; border-radius:8px; }
|
||||||
|
.controls { display:flex; gap:8px; margin:8px 0; }
|
||||||
|
.btn { padding:8px 12px; border:none; border-radius:6px; background:#e3f2fd; color:#1565c0; cursor:pointer; }
|
||||||
|
.offer-view { font-size: 14px; color:#333; }
|
||||||
|
.hint { color:#666; }
|
||||||
|
</style>
|
||||||
40
client/src/views/games/G2.vue
Normal file
40
client/src/views/games/G2.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div class="g">
|
||||||
|
<h3>G2 – Regla contraproductiva (P2 puede forzar)</h3>
|
||||||
|
<div class="controls" v-if="myRole === 'P2'">
|
||||||
|
<label><input type="checkbox" :checked="state.forcedByP2" @change="$emit('p2Force', ($event.target as HTMLInputElement).checked)"/> Forzar oferta</label>
|
||||||
|
</div>
|
||||||
|
<OfferControls v-if="myRole==='P1' && !state.offer?.active" :disable-no-offer="state.forcedByP2" @propose="onPropose" @no-offer="onNoOffer"/>
|
||||||
|
<div v-if="state.offer?.active" class="note">Oferta activa</div>
|
||||||
|
<div v-if="state.offer?.active" class="controls">
|
||||||
|
<div v-if="myRole === 'P2'">
|
||||||
|
<button class="btn" @click="$emit('p2Action', 'accept')">P2: Aceptar</button>
|
||||||
|
<button class="btn" @click="$emit('p2Action', 'reject')">P2: Rechazar</button>
|
||||||
|
<button class="btn" @click="$emit('p2Action', 'snatch')">P2: Robar</button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="hint">Esperando decisión de P2…</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import OfferControls from './OfferControls.vue';
|
||||||
|
const props = defineProps<{ state: any; myRole: string }>();
|
||||||
|
const emit = defineEmits(['p2Force','p1Action','p2Action','proposeOffer']);
|
||||||
|
function onPropose(payload: any) {
|
||||||
|
emit('proposeOffer', payload);
|
||||||
|
}
|
||||||
|
function onNoOffer() {
|
||||||
|
if (!props.state.forcedByP2) {
|
||||||
|
emit('p1Action', 'no_offer');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.g { background:#fff; padding:12px; border-radius:8px; }
|
||||||
|
.controls { display:flex; gap:8px; margin:8px 0; }
|
||||||
|
.btn { padding:8px 12px; border:none; border-radius:6px; background:#e3f2fd; color:#1565c0; cursor:pointer; }
|
||||||
|
.note { color:#1565c0; font-weight:600; }
|
||||||
|
.hint { color:#666; }
|
||||||
|
</style>
|
||||||
38
client/src/views/games/G3.vue
Normal file
38
client/src/views/games/G3.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<div class="g">
|
||||||
|
<h3>G3 – Token de repudio (vergüenza)</h3>
|
||||||
|
<OfferControls v-if="myRole==='P1' && !state.offer?.active" @propose="onPropose" @no-offer="onNoOffer"/>
|
||||||
|
<div v-if="state.offer?.active" class="controls">
|
||||||
|
<div v-if="myRole === 'P2'">
|
||||||
|
<button class="btn" @click="$emit('p2Action', 'accept')">P2: Aceptar</button>
|
||||||
|
<button class="btn" @click="$emit('p2Action', 'reject')">P2: Rechazar</button>
|
||||||
|
<button class="btn" @click="$emit('p2Action', 'snatch')">P2: Robar</button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="hint">Esperando decisión de P2…</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="state.p2Action === 'snatch' && myRole === 'P1'" class="controls">
|
||||||
|
<button class="btn warn" @click="$emit('assignShame', true)">Asignar vergüenza</button>
|
||||||
|
<button class="btn" @click="$emit('assignShame', false)">No asignar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import OfferControls from './OfferControls.vue';
|
||||||
|
const props = defineProps<{ state: any; myRole: string }>();
|
||||||
|
const emit = defineEmits(['p1Action','p2Action','assignShame','proposeOffer']);
|
||||||
|
function onPropose(payload: any) {
|
||||||
|
emit('proposeOffer', payload);
|
||||||
|
}
|
||||||
|
function onNoOffer() {
|
||||||
|
emit('p1Action', 'no_offer');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.g { background:#fff; padding:12px; border-radius:8px; }
|
||||||
|
.controls { display:flex; gap:8px; margin:8px 0; }
|
||||||
|
.btn { padding:8px 12px; border:none; border-radius:6px; background:#e3f2fd; color:#1565c0; cursor:pointer; }
|
||||||
|
.btn.warn { background:#ffecb3; color:#8d6e63; }
|
||||||
|
.hint { color:#666; }
|
||||||
|
</style>
|
||||||
38
client/src/views/games/G4.vue
Normal file
38
client/src/views/games/G4.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<div class="g">
|
||||||
|
<h3>G4 – Derechos mínimos de propiedad (juez)</h3>
|
||||||
|
<OfferControls v-if="myRole==='P1' && !state.offer?.active" @propose="onPropose" @no-offer="onNoOffer"/>
|
||||||
|
<div v-if="state.offer?.active" class="controls">
|
||||||
|
<div v-if="myRole === 'P2'">
|
||||||
|
<button class="btn" @click="$emit('p2Action', 'accept')">P2: Aceptar</button>
|
||||||
|
<button class="btn" @click="$emit('p2Action', 'reject')">P2: Rechazar</button>
|
||||||
|
<button class="btn" @click="$emit('p2Action', 'snatch')">P2: Robar</button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="hint">Esperando decisión de P2…</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="state.p2Action === 'snatch' && myRole === 'P1'" class="controls">
|
||||||
|
<button class="btn warn" @click="$emit('report', true)">Denunciar (confiscar tokens)</button>
|
||||||
|
<button class="btn" @click="$emit('report', false)">No denunciar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import OfferControls from './OfferControls.vue';
|
||||||
|
const props = defineProps<{ state: any; myRole: string }>();
|
||||||
|
const emit = defineEmits(['p1Action','p2Action','report','proposeOffer']);
|
||||||
|
function onPropose(payload: any) {
|
||||||
|
emit('proposeOffer', payload);
|
||||||
|
}
|
||||||
|
function onNoOffer() {
|
||||||
|
emit('p1Action', 'no_offer');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.g { background:#fff; padding:12px; border-radius:8px; }
|
||||||
|
.controls { display:flex; gap:8px; margin:8px 0; }
|
||||||
|
.btn { padding:8px 12px; border:none; border-radius:6px; background:#e3f2fd; color:#1565c0; cursor:pointer; }
|
||||||
|
.btn.warn { background:#ffe0e0; color:#b71c1c; }
|
||||||
|
.hint { color:#666; }
|
||||||
|
</style>
|
||||||
50
client/src/views/games/G5.vue
Normal file
50
client/src/views/games/G5.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<div class="g">
|
||||||
|
<h3>G5 – Cheap talk (chat previo no vinculante)</h3>
|
||||||
|
<div class="chat">
|
||||||
|
<input v-model="msg" placeholder="Mensaje (no vinculante)" />
|
||||||
|
<button class="btn" @click="send">Enviar</button>
|
||||||
|
</div>
|
||||||
|
<OfferControls v-if="myRole==='P1' && !state.offer?.active" @propose="onPropose" @no-offer="onNoOffer"/>
|
||||||
|
<div v-if="state.offer?.active" class="controls">
|
||||||
|
<div v-if="myRole === 'P2'">
|
||||||
|
<button class="btn" @click="$emit('p2Action', 'accept')">P2: Aceptar</button>
|
||||||
|
<button class="btn" @click="$emit('p2Action', 'reject')">P2: Rechazar</button>
|
||||||
|
<button class="btn" @click="$emit('p2Action', 'snatch')">P2: Robar</button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="hint">Esperando decisión de P2…</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { colyseusService } from '../../services/colyseus';
|
||||||
|
import OfferControls from './OfferControls.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{ state: any; myRole: string }>();
|
||||||
|
const emit = defineEmits(['p1Action','p2Action','proposeOffer']);
|
||||||
|
|
||||||
|
const msg = ref('');
|
||||||
|
function send() {
|
||||||
|
// For MVP, just log locally; can be wired to room message later
|
||||||
|
console.log('cheap talk:', msg.value);
|
||||||
|
msg.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPropose(payload: any) {
|
||||||
|
emit('proposeOffer', payload);
|
||||||
|
}
|
||||||
|
function onNoOffer() {
|
||||||
|
emit('p1Action', 'no_offer');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.g { background:#fff; padding:12px; border-radius:8px; }
|
||||||
|
.controls { display:flex; gap:8px; margin:8px 0; }
|
||||||
|
.chat { display:flex; gap:8px; margin:8px 0; }
|
||||||
|
.chat input { flex:1; padding:8px; border-radius:6px; border:1px solid #ddd; }
|
||||||
|
.btn { padding:8px 12px; border:none; border-radius:6px; background:#e3f2fd; color:#1565c0; cursor:pointer; }
|
||||||
|
.hint { color:#666; }
|
||||||
|
</style>
|
||||||
65
client/src/views/games/OfferControls.vue
Normal file
65
client/src/views/games/OfferControls.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div class="offer">
|
||||||
|
<div class="row">
|
||||||
|
<label>Ofrezco:</label>
|
||||||
|
<input type="number" min="0" v-model.number="offerPavo" /> 🦃
|
||||||
|
<input type="number" min="0" v-model.number="offerElote" /> 🌽
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>A cambio:</label>
|
||||||
|
<input type="number" min="0" v-model.number="requestPavo" /> 🦃
|
||||||
|
<input type="number" min="0" v-model.number="requestElote" /> 🌽
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<button class="btn primary" @click="propose">Enviar oferta</button>
|
||||||
|
<button class="btn" @click="noOffer" :disabled="disableNoOffer">No ofrecer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
const props = defineProps<{ disableNoOffer?: boolean }>();
|
||||||
|
const emit = defineEmits(['propose','no-offer']);
|
||||||
|
const offerPavo = ref(0);
|
||||||
|
const offerElote = ref(0);
|
||||||
|
const requestPavo = ref(0);
|
||||||
|
const requestElote = ref(0);
|
||||||
|
|
||||||
|
function propose() {
|
||||||
|
// Always emit the proposal with current values
|
||||||
|
const payload = {
|
||||||
|
offerPavo: Math.max(0, offerPavo.value|0),
|
||||||
|
offerElote: Math.max(0, offerElote.value|0),
|
||||||
|
requestPavo: Math.max(0, requestPavo.value|0),
|
||||||
|
requestElote: Math.max(0, requestElote.value|0)
|
||||||
|
};
|
||||||
|
emit('propose', payload);
|
||||||
|
|
||||||
|
// Clear inputs after sending
|
||||||
|
offerPavo.value = 0;
|
||||||
|
offerElote.value = 0;
|
||||||
|
requestPavo.value = 0;
|
||||||
|
requestElote.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function noOffer() {
|
||||||
|
// Clear inputs
|
||||||
|
offerPavo.value = 0;
|
||||||
|
offerElote.value = 0;
|
||||||
|
requestPavo.value = 0;
|
||||||
|
requestElote.value = 0;
|
||||||
|
emit('no-offer');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.offer { background:#f9fafb; padding:10px; border-radius:8px; }
|
||||||
|
.row { display:flex; align-items:center; gap:8px; margin-bottom:8px; }
|
||||||
|
label { width:70px; color:#555; }
|
||||||
|
input { width:80px; padding:6px; border:1px solid #ddd; border-radius:6px; }
|
||||||
|
.controls { display:flex; gap:8px; }
|
||||||
|
.btn { padding:6px 10px; border:none; border-radius:6px; background:#e3f2fd; color:#1565c0; cursor:pointer; }
|
||||||
|
.btn:disabled { opacity:0.5; cursor:not-allowed; }
|
||||||
|
.btn.primary { background:#667eea; color:#fff; }
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
flowchart TD
|
flowchart TD
|
||||||
A1[P1: Ofrecer 5?] -->|No ofrecer| O1[10,10]
|
A1[P1: Proponer oferta? (pavos/elotes + pedido)] -->|No ofrecer| O1[Sin cambios]
|
||||||
A1 -->|Ofrecer| B1[P2: Aceptar / Rechazar / Robar]
|
A1 -->|Ofrecer| B1[P2: Aceptar / Rechazar / Robar]
|
||||||
B1 -->|Aceptar| O2[15,15]
|
B1 -->|Aceptar| O2[Intercambiar según oferta/pedido]
|
||||||
B1 -->|Rechazar| O3[10,10]
|
B1 -->|Rechazar| O3[Sin cambios]
|
||||||
B1 -->|Robar| O4[5,20]
|
B1 -->|Robar| O4[Transferir solo lo ofrecido a P2]
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
flowchart TD
|
flowchart TD
|
||||||
A2[P2: Forzar?] -->|Sí| F2[P1: Oferta forzada]
|
A2[P2: Forzar?] -->|Sí| F2[P1: Debe proponer oferta]
|
||||||
A2 -->|No| B2[P1: Ofrecer 5?]
|
A2 -->|No| B2[P1: Proponer oferta?]
|
||||||
F2 --> C2[P2: Acción final]
|
F2 --> C2[P2: Acción final]
|
||||||
B2 -->|No ofrecer| O1[10,10]
|
B2 -->|No ofrecer| O1[Sin cambios]
|
||||||
B2 -->|Ofrecer| C2[P2: Aceptar / Rechazar / Robar]
|
B2 -->|Ofrecer| C2[P2: Aceptar / Rechazar / Robar]
|
||||||
C2 -->|Aceptar| O2[15,15]
|
C2 -->|Aceptar| O2[Intercambiar según oferta/pedido]
|
||||||
C2 -->|Rechazar| O3[10,10]
|
C2 -->|Rechazar| O3[Sin cambios]
|
||||||
C2 -->|Robar| O4[5,20]
|
C2 -->|Robar| O4[Transferir solo lo ofrecido a P2]
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
flowchart TD
|
flowchart TD
|
||||||
A3[P1: Ofrecer 5?] -->|No ofrecer| O1[10,10]
|
A3[P1: Proponer oferta?] -->|No ofrecer| O1[Sin cambios]
|
||||||
A3 -->|Ofrecer| B3[P2: Aceptar / Rechazar / Robar]
|
A3 -->|Ofrecer| B3[P2: Aceptar / Rechazar / Robar]
|
||||||
B3 -->|Aceptar| O2[15,15]
|
B3 -->|Aceptar| O2[Intercambiar según oferta/pedido]
|
||||||
B3 -->|Rechazar| O3[10,10]
|
B3 -->|Rechazar| O3[Sin cambios]
|
||||||
B3 -->|Robar| C3[P1: Asignar ficha de verguenza?]
|
B3 -->|Robar| C3[P1: Asignar ficha de vergüenza?]
|
||||||
C3 -->|Sí| O4a[5,20 +1 verguenza proxima partida]
|
C3 -->|Sí| O4a[+1 vergüenza para P2]
|
||||||
C3 -->|No| O4b[5,20]
|
C3 -->|No| O4b[Sin vergüenza]
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
flowchart TD
|
flowchart TD
|
||||||
A4[P1: Ofrecer 5?] -->|No ofrecer| O1[10,10]
|
A4[P1: Proponer oferta?] -->|No ofrecer| O1[Sin cambios]
|
||||||
A4 -->|Ofrecer| B4[P2: Aceptar / Rechazar / Robar]
|
A4 -->|Ofrecer| B4[P2: Aceptar / Rechazar / Robar]
|
||||||
B4 -->|Aceptar| O2[15,15]
|
B4 -->|Aceptar| O2[Intercambiar según oferta/pedido]
|
||||||
B4 -->|Rechazar| O3[10,10]
|
B4 -->|Rechazar| O3[Sin cambios]
|
||||||
B4 -->|Robar| C4[P1: ¿Denunciar?]
|
B4 -->|Robar| C4[P1: ¿Denunciar?]
|
||||||
C4 -->|No| O4[5,20]
|
C4 -->|No| O4[Transferir solo lo ofrecido a P2]
|
||||||
C4 -->|Sí| J4[AutoJudge confisca tokens P2]
|
C4 -->|Sí| J4[AutoJudge revierte robo (confisca oferta a P2)]
|
||||||
J4 --> O5[10,0]
|
J4 --> O5[Restituir oferta a P1]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
flowchart TD
|
flowchart TD
|
||||||
Pre[Chat previo 1 min - no vinculante] --> A5[P1: Ofrecer 5?]
|
Pre[Chat previo 1 min - no vinculante] --> A5[P1: Proponer oferta?]
|
||||||
A5 -->|No ofrecer| O1[10,10]
|
A5 -->|No ofrecer| O1[Sin cambios]
|
||||||
A5 -->|Ofrecer| B5[P2: Aceptar / Rechazar / Robar]
|
A5 -->|Ofrecer| B5[P2: Aceptar / Rechazar / Robar]
|
||||||
B5 -->|Aceptar| O2[15,15]
|
B5 -->|Aceptar| O2[Intercambiar según oferta/pedido]
|
||||||
B5 -->|Rechazar| O3[10,10]
|
B5 -->|Rechazar| O3[Sin cambios]
|
||||||
B5 -->|Robar| O4[5,20]
|
B5 -->|Robar| O4[Transferir solo lo ofrecido a P2]
|
||||||
|
|||||||
@@ -102,110 +102,106 @@ sequenceDiagram
|
|||||||
Note over P1,P2: Sin decision previa de P2
|
Note over P1,P2: Sin decision previa de P2
|
||||||
end
|
end
|
||||||
|
|
||||||
P1->>S: actionP1(offer or no_offer) - o forzado en G2
|
|
||||||
S-->>P2: notifyP1Action(offer or no_offer)
|
|
||||||
|
|
||||||
alt no_offer
|
alt no_offer
|
||||||
S-->>P1: outcome(10,10)
|
P1->>S: noOffer()
|
||||||
S-->>P2: outcome(10,10)
|
S-->>P1: sin cambios de tokens
|
||||||
else offer
|
S-->>P2: sin cambios de tokens
|
||||||
|
else oferta
|
||||||
|
P1->>S: proposeOffer({offer:{pavo,elote}, request:{pavo,elote}})
|
||||||
|
S-->>P2: offerAvailable
|
||||||
P2->>S: actionP2(accept / reject / snatch)
|
P2->>S: actionP2(accept / reject / snatch)
|
||||||
alt accept
|
alt accept
|
||||||
S-->>P1: outcome(15,15)
|
S-->>P1: transfer ambos lados (según oferta/pedido)
|
||||||
S-->>P2: outcome(15,15)
|
S-->>P2: transfer ambos lados (según oferta/pedido)
|
||||||
else reject
|
else reject
|
||||||
S-->>P1: outcome(10,10)
|
S-->>P1: sin cambios
|
||||||
S-->>P2: outcome(10,10)
|
S-->>P2: sin cambios
|
||||||
else snatch
|
else snatch
|
||||||
|
S-->>P2: transferir solo lo ofrecido a P2
|
||||||
opt G4 denuncia
|
opt G4 denuncia
|
||||||
P1->>S: report: yes or no
|
P1->>S: report: yes or no
|
||||||
alt report=yes
|
alt report=yes
|
||||||
S->>AJ: aplicar sancion
|
S->>AJ: aplicar sanción
|
||||||
AJ-->>S: confiscar tokens P2
|
AJ-->>S: confiscar oferta a P2 y revertir a P1
|
||||||
S-->>P1: outcome(10,0)
|
|
||||||
S-->>P2: outcome(10,0)
|
|
||||||
else report=no
|
else report=no
|
||||||
S-->>P1: outcome(5,20)
|
Note over P1,P2: Se mantiene el robo
|
||||||
S-->>P2: outcome(5,20)
|
|
||||||
end
|
end
|
||||||
|
end
|
||||||
opt G3 repudio
|
opt G3 repudio
|
||||||
P1->>S: shameToken: assign yes or no
|
P1->>S: shameToken: assign yes or no
|
||||||
S-->>P2: actualizar contador verguenza (proxima partida)
|
S-->>P2: actualizar contador vergüenza (próxima partida)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
opt Round3 - persistir resultado R3
|
|
||||||
S->>S: actualizar leaderboard y analytics
|
|
||||||
end
|
|
||||||
S-->>P1: endRound
|
S-->>P1: endRound
|
||||||
S-->>P2: endRound
|
S-->>P2: endRound
|
||||||
```
|
```
|
||||||
|
|
||||||
## Variantes de juego
|
## Variantes de juego
|
||||||
|
|
||||||
### G1 – Sin derechos de propiedad
|
### G1 – Sin derechos de propiedad (oferta variable)
|
||||||
```mermaid
|
```mermaid
|
||||||
%% g1-no-property.mmd
|
%% g1-no-property.mmd
|
||||||
flowchart TD
|
flowchart TD
|
||||||
A1[P1: Ofrecer 5?] -->|No ofrecer| O1[10,10]
|
A1[P1: Proponer oferta? (pavos/elotes + pedido)] -->|No ofrecer| O1[Sin cambios]
|
||||||
A1 -->|Ofrecer| B1[P2: Aceptar / Rechazar / Robar]
|
A1 -->|Ofrecer| B1[P2: Aceptar / Rechazar / Robar]
|
||||||
B1 -->|Aceptar| O2[15,15]
|
B1 -->|Aceptar| O2[Intercambiar según oferta/pedido]
|
||||||
B1 -->|Rechazar| O3[10,10]
|
B1 -->|Rechazar| O3[Sin cambios]
|
||||||
B1 -->|Robar| O4[5,20]
|
B1 -->|Robar| O4[Transferir solo lo ofrecido a P2]
|
||||||
```
|
```
|
||||||
|
|
||||||
### G2 – Regla contraproductiva (P2 puede forzar)
|
### G2 – Regla contraproductiva (P2 puede forzar) – oferta variable
|
||||||
```mermaid
|
```mermaid
|
||||||
%% g2-counterproductive-rule.mmd
|
%% g2-counterproductive-rule.mmd
|
||||||
flowchart TD
|
flowchart TD
|
||||||
A2[P2: Forzar?] -->|Sí| F2[P1: Oferta forzada]
|
A2[P2: Forzar?] -->|Sí| F2[P1: Debe proponer oferta]
|
||||||
A2 -->|No| B2[P1: Ofrecer 5?]
|
A2 -->|No| B2[P1: Proponer oferta?]
|
||||||
F2 --> C2[P2: Acción final]
|
F2 --> C2[P2: Acción final]
|
||||||
B2 -->|No ofrecer| O1[10,10]
|
B2 -->|No ofrecer| O1[Sin cambios]
|
||||||
B2 -->|Ofrecer| C2[P2: Aceptar / Rechazar / Robar]
|
B2 -->|Ofrecer| C2[P2: Aceptar / Rechazar / Robar]
|
||||||
C2 -->|Aceptar| O2[15,15]
|
C2 -->|Aceptar| O2[Intercambiar según oferta/pedido]
|
||||||
C2 -->|Rechazar| O3[10,10]
|
C2 -->|Rechazar| O3[Sin cambios]
|
||||||
C2 -->|Robar| O4[5,20]
|
C2 -->|Robar| O4[Transferir solo lo ofrecido a P2]
|
||||||
```
|
```
|
||||||
|
|
||||||
### G3 – Token de repudio (vergüenza)
|
### G3 – Token de repudio (vergüenza) – oferta variable
|
||||||
```mermaid
|
```mermaid
|
||||||
%% g3-shame-token.mmd
|
%% g3-shame-token.mmd
|
||||||
flowchart TD
|
flowchart TD
|
||||||
A3[P1: Ofrecer 5?] -->|No ofrecer| O1[10,10]
|
A3[P1: Proponer oferta?] -->|No ofrecer| O1[Sin cambios]
|
||||||
A3 -->|Ofrecer| B3[P2: Aceptar / Rechazar / Robar]
|
A3 -->|Ofrecer| B3[P2: Aceptar / Rechazar / Robar]
|
||||||
B3 -->|Aceptar| O2[15,15]
|
B3 -->|Aceptar| O2[Intercambiar según oferta/pedido]
|
||||||
B3 -->|Rechazar| O3[10,10]
|
B3 -->|Rechazar| O3[Sin cambios]
|
||||||
B3 -->|Robar| C3[P1: Asignar ficha de verguenza?]
|
B3 -->|Robar| C3[P1: Asignar ficha de vergüenza?]
|
||||||
C3 -->|Sí| O4a[5,20 +1 verguenza proxima partida]
|
C3 -->|Sí| O4a[+1 vergüenza para P2]
|
||||||
C3 -->|No| O4b[5,20]
|
C3 -->|No| O4b[Sin vergüenza]
|
||||||
```
|
```
|
||||||
|
|
||||||
### G4 – Derechos mínimos de propiedad (juez)
|
### G4 – Derechos mínimos de propiedad (juez) – oferta variable
|
||||||
```mermaid
|
```mermaid
|
||||||
%% g4-min-property-rights.mmd
|
%% g4-min-property-rights.mmd
|
||||||
flowchart TD
|
flowchart TD
|
||||||
A4[P1: Ofrecer 5?] -->|No ofrecer| O1[10,10]
|
A4[P1: Proponer oferta?] -->|No ofrecer| O1[Sin cambios]
|
||||||
A4 -->|Ofrecer| B4[P2: Aceptar / Rechazar / Robar]
|
A4 -->|Ofrecer| B4[P2: Aceptar / Rechazar / Robar]
|
||||||
B4 -->|Aceptar| O2[15,15]
|
B4 -->|Aceptar| O2[Intercambiar según oferta/pedido]
|
||||||
B4 -->|Rechazar| O3[10,10]
|
B4 -->|Rechazar| O3[Sin cambios]
|
||||||
B4 -->|Robar| C4[P1: ¿Denunciar?]
|
B4 -->|Robar| C4[P1: ¿Denunciar?]
|
||||||
C4 -->|No| O4[5,20]
|
C4 -->|No| O4[Transferir solo lo ofrecido a P2]
|
||||||
C4 -->|Sí| J4[Juez confisca tokens P2]
|
C4 -->|Sí| J4[AutoJudge revierte robo (confisca oferta a P2)]
|
||||||
J4 --> O5[10,0]
|
J4 --> O5[Restituir oferta a P1]
|
||||||
```
|
```
|
||||||
|
|
||||||
### G5 – Cheap talk (conversación previa)
|
### G5 – Cheap talk (conversación previa) – oferta variable
|
||||||
```mermaid
|
```mermaid
|
||||||
%% g5-cheap-talk.mmd
|
%% g5-cheap-talk.mmd
|
||||||
flowchart TD
|
flowchart TD
|
||||||
Pre[Chat previo 1 min - no vinculante] --> A5[P1: Ofrecer 5?]
|
Pre[Chat previo 1 min - no vinculante] --> A5[P1: Proponer oferta?]
|
||||||
A5 -->|No ofrecer| O1[10,10]
|
A5 -->|No ofrecer| O1[Sin cambios]
|
||||||
A5 -->|Ofrecer| B5[P2: Aceptar / Rechazar / Robar]
|
A5 -->|Ofrecer| B5[P2: Aceptar / Rechazar / Robar]
|
||||||
B5 -->|Aceptar| O2[15,15]
|
B5 -->|Aceptar| O2[Intercambiar según oferta/pedido]
|
||||||
B5 -->|Rechazar| O3[10,10]
|
B5 -->|Rechazar| O3[Sin cambios]
|
||||||
B5 -->|Robar| O4[5,20]
|
B5 -->|Robar| O4[Transferir solo lo ofrecido a P2]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Emparejamiento en masa (fase Gx)
|
## Emparejamiento en masa (fase Gx)
|
||||||
|
|||||||
@@ -6,16 +6,137 @@ import { NameManager } from "../utils/nameManager";
|
|||||||
export class GameRoom extends Room<GameState> {
|
export class GameRoom extends Room<GameState> {
|
||||||
maxClients = 2;
|
maxClients = 2;
|
||||||
private gameInterval?: NodeJS.Timeout;
|
private gameInterval?: NodeJS.Timeout;
|
||||||
private readonly TICK_RATE = 1000; // Update every second
|
|
||||||
|
|
||||||
onCreate(options: any) {
|
onCreate(options: any) {
|
||||||
this.setState(new GameState());
|
this.setState(new GameState());
|
||||||
this.state.roomId = this.roomId;
|
this.state.roomId = this.roomId;
|
||||||
|
|
||||||
this.onMessage("click", (client) => {
|
// Variant selection (both players can change)
|
||||||
this.handleClick(client);
|
this.onMessage("setVariant", (client, variant: string) => {
|
||||||
|
this.state.currentVariant = variant;
|
||||||
|
// Reset to round 1 and clear decisions when variant changes
|
||||||
|
this.state.currentRound = 1;
|
||||||
|
this.state.resetRound();
|
||||||
|
// G2: Force offer by default
|
||||||
|
if (variant === 'G2') {
|
||||||
|
this.state.forcedByP2 = true;
|
||||||
|
}
|
||||||
|
this.broadcast("variantChanged", { variant });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// P1 proposes a variable offer (offer -> P2, request <- from P2)
|
||||||
|
this.onMessage("proposeOffer", (client, payload: { offerPavo:number; offerElote:number; requestPavo:number; requestElote:number; }) => {
|
||||||
|
const player = this.state.players.get(client.sessionId);
|
||||||
|
if (!player || player.role !== "P1") return;
|
||||||
|
const p1 = this.state.p1Id ? this.state.players.get(this.state.p1Id) : undefined;
|
||||||
|
const p2 = this.state.p2Id ? this.state.players.get(this.state.p2Id) : undefined;
|
||||||
|
if (!p1 || !p2) return;
|
||||||
|
|
||||||
|
const oPavo = Math.max(0, Math.floor(payload.offerPavo || 0));
|
||||||
|
const oElote = Math.max(0, Math.floor(payload.offerElote || 0));
|
||||||
|
const rPavo = Math.max(0, Math.floor(payload.requestPavo || 0));
|
||||||
|
const rElote = Math.max(0, Math.floor(payload.requestElote || 0));
|
||||||
|
|
||||||
|
// Validate holdings: P1 must have offered tokens; P2 must have requested tokens
|
||||||
|
if (oPavo > p1.pavoTokens) return;
|
||||||
|
if (oElote > p1.eloteTokens) return;
|
||||||
|
if (rPavo > p2.pavoTokens) return;
|
||||||
|
if (rElote > p2.eloteTokens) return;
|
||||||
|
|
||||||
|
// Clear any previous state before setting new offer
|
||||||
|
this.state.resetRound();
|
||||||
|
|
||||||
|
this.state.offerPavo = oPavo;
|
||||||
|
this.state.offerElote = oElote;
|
||||||
|
this.state.requestPavo = rPavo;
|
||||||
|
this.state.requestElote = rElote;
|
||||||
|
this.state.offerActive = true; // Always set active when an offer is proposed
|
||||||
|
this.state.p1Action = "offer";
|
||||||
|
});
|
||||||
|
|
||||||
|
// P1 decides to not offer
|
||||||
|
this.onMessage("noOffer", (client) => {
|
||||||
|
const player = this.state.players.get(client.sessionId);
|
||||||
|
if (!player || player.role !== "P1") return;
|
||||||
|
if (this.state.forcedByP2) return; // cannot refuse if forced in G2
|
||||||
|
if (this.state.offerActive) return; // Can't "no offer" if offer is already active
|
||||||
|
|
||||||
|
this.state.resetRound();
|
||||||
|
this.state.p1Action = "no_offer";
|
||||||
|
// Auto-advance to next round when P1 doesn't offer
|
||||||
|
this.advanceRound();
|
||||||
|
});
|
||||||
|
|
||||||
|
// G2: P2 may force an offer
|
||||||
|
this.onMessage("p2Force", (client, force: boolean) => {
|
||||||
|
const player = this.state.players.get(client.sessionId);
|
||||||
|
if (!player) return;
|
||||||
|
if (player.role !== "P2") return;
|
||||||
|
this.state.forcedByP2 = !!force;
|
||||||
|
// When forced, P1 must propose an offer; nothing automatic here.
|
||||||
|
});
|
||||||
|
|
||||||
|
// P2 action
|
||||||
|
this.onMessage("p2Action", (client, action: string) => {
|
||||||
|
const player = this.state.players.get(client.sessionId);
|
||||||
|
if (!player) return;
|
||||||
|
if (player.role !== "P2") return;
|
||||||
|
this.state.p2Action = action; // accept | reject | snatch
|
||||||
|
this.resolveP2Action();
|
||||||
|
|
||||||
|
// Auto-advance unless it's a snatch in G3 or G4 (need shame/report)
|
||||||
|
if (action !== 'snatch' || (this.state.currentVariant !== 'G3' && this.state.currentVariant !== 'G4')) {
|
||||||
|
this.advanceRound();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// G4 report after snatch
|
||||||
|
this.onMessage("report", (client, report: boolean) => {
|
||||||
|
const player = this.state.players.get(client.sessionId);
|
||||||
|
if (!player) return;
|
||||||
|
if (player.role !== "P1") return;
|
||||||
|
this.state.reported = !!report;
|
||||||
|
if (report && this.state.currentVariant === "G4" && this.state.p2Action === "snatch") {
|
||||||
|
// Inverse of snatch: P1 gets requested without giving offered
|
||||||
|
const p1 = this.state.p1Id ? this.state.players.get(this.state.p1Id) : undefined;
|
||||||
|
const p2 = this.state.p2Id ? this.state.players.get(this.state.p2Id) : undefined;
|
||||||
|
if (p1 && p2) {
|
||||||
|
// First, revert the snatch (return offered tokens to P1)
|
||||||
|
const oP = this.state.offerPavo;
|
||||||
|
const oE = this.state.offerElote;
|
||||||
|
if (p2.pavoTokens >= oP) { p2.pavoTokens -= oP; p1.pavoTokens += oP; }
|
||||||
|
if (p2.eloteTokens >= oE) { p2.eloteTokens -= oE; p1.eloteTokens += oE; }
|
||||||
|
|
||||||
|
// Then apply the sanction: P1 gets requested without giving anything
|
||||||
|
const rP = this.state.requestPavo;
|
||||||
|
const rE = this.state.requestElote;
|
||||||
|
if (p2.pavoTokens >= rP) { p2.pavoTokens -= rP; p1.pavoTokens += rP; }
|
||||||
|
if (p2.eloteTokens >= rE) { p2.eloteTokens -= rE; p1.eloteTokens += rE; }
|
||||||
|
}
|
||||||
|
// Clear offer now
|
||||||
|
this.clearOffer();
|
||||||
|
}
|
||||||
|
// Auto-advance after report decision
|
||||||
|
this.advanceRound();
|
||||||
|
});
|
||||||
|
|
||||||
|
// G3 shame token after snatch
|
||||||
|
this.onMessage("assignShame", (client, assign: boolean) => {
|
||||||
|
const player = this.state.players.get(client.sessionId);
|
||||||
|
if (!player) return;
|
||||||
|
if (player.role !== "P1") return;
|
||||||
|
this.state.shameAssigned = !!assign;
|
||||||
|
if (assign && this.state.currentVariant === "G3" && this.state.p2Action === "snatch") {
|
||||||
|
// increment P2 shame immediately
|
||||||
|
const p2 = this.state.p2Id ? this.state.players.get(this.state.p2Id) : undefined;
|
||||||
|
if (p2) p2.shameTokens += 1;
|
||||||
|
}
|
||||||
|
// Auto-advance after shame decision
|
||||||
|
this.advanceRound();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Removed nextRound handler - rounds now auto-advance
|
||||||
|
|
||||||
this.onMessage("admin:pause", () => {
|
this.onMessage("admin:pause", () => {
|
||||||
this.state.pauseGame();
|
this.state.pauseGame();
|
||||||
});
|
});
|
||||||
@@ -39,7 +160,7 @@ export class GameRoom extends Room<GameState> {
|
|||||||
// Use the playerName passed from the lobby - don't generate a new one!
|
// Use the playerName passed from the lobby - don't generate a new one!
|
||||||
const playerName = options.playerName || "player";
|
const playerName = options.playerName || "player";
|
||||||
|
|
||||||
this.state.addPlayer(client.sessionId, playerName);
|
const player = this.state.addPlayer(client.sessionId, playerName);
|
||||||
|
|
||||||
client.send("playerInfo", {
|
client.send("playerInfo", {
|
||||||
sessionId: client.sessionId,
|
sessionId: client.sessionId,
|
||||||
@@ -94,18 +215,13 @@ export class GameRoom extends Room<GameState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private startGame() {
|
private startGame() {
|
||||||
console.log(`[GameRoom] Starting game in room ${this.roomId}`);
|
console.log(`[GameRoom] Starting demo game in room ${this.roomId}`);
|
||||||
|
|
||||||
this.state.startGame();
|
this.state.startGame();
|
||||||
|
// G2: Force offer by default when starting game
|
||||||
|
if (this.state.currentVariant === 'G2') {
|
||||||
|
this.state.forcedByP2 = true;
|
||||||
|
}
|
||||||
this.broadcast("gameStart");
|
this.broadcast("gameStart");
|
||||||
|
|
||||||
this.gameInterval = setInterval(() => {
|
|
||||||
this.state.updateTimer(this.TICK_RATE / 1000);
|
|
||||||
|
|
||||||
if (this.state.gameStatus === GameStatus.FINISHED) {
|
|
||||||
this.endGame();
|
|
||||||
}
|
|
||||||
}, this.TICK_RATE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private pauseGame() {
|
private pauseGame() {
|
||||||
@@ -115,38 +231,56 @@ export class GameRoom extends Room<GameState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private endGame() {
|
private endGame() {
|
||||||
console.log(`[GameRoom] Game ended in room ${this.roomId}. Winner: ${this.state.winner}`);
|
console.log(`[GameRoom] Demo game ended in room ${this.roomId}`);
|
||||||
|
this.broadcast("gameEnd", {});
|
||||||
if (this.gameInterval) {
|
|
||||||
clearInterval(this.gameInterval);
|
|
||||||
this.gameInterval = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.broadcast("gameEnd", {
|
|
||||||
winner: this.state.winner,
|
|
||||||
players: Array.from(this.state.players.values()).map(p => ({
|
|
||||||
name: p.name,
|
|
||||||
clicks: p.clicks
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.state.restartGame();
|
|
||||||
if (this.state.players.size === 2) {
|
|
||||||
this.startGame();
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleClick(client: Client) {
|
private resolveP2Action() {
|
||||||
if (this.state.gameStatus !== GameStatus.PLAYING) {
|
const p1 = this.state.p1Id ? this.state.players.get(this.state.p1Id) : undefined;
|
||||||
|
const p2 = this.state.p2Id ? this.state.players.get(this.state.p2Id) : undefined;
|
||||||
|
if (!p1 || !p2) return;
|
||||||
|
const { p2Action, offerActive } = this.state;
|
||||||
|
if (!offerActive && this.state.p1Action !== 'no_offer') return;
|
||||||
|
|
||||||
|
if (this.state.p1Action === 'no_offer') {
|
||||||
|
// Nothing to transfer; round can proceed.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = this.state.players.get(client.sessionId);
|
if (p2Action === 'accept') {
|
||||||
if (player && player.connected) {
|
// Transfer P1 -> P2 (offered)
|
||||||
player.incrementClicks();
|
if (p1.pavoTokens >= this.state.offerPavo && p1.eloteTokens >= this.state.offerElote &&
|
||||||
|
p2.pavoTokens >= this.state.requestPavo && p2.eloteTokens >= this.state.requestElote) {
|
||||||
|
p1.pavoTokens -= this.state.offerPavo; p2.pavoTokens += this.state.offerPavo;
|
||||||
|
p1.eloteTokens -= this.state.offerElote; p2.eloteTokens += this.state.offerElote;
|
||||||
|
// Transfer P2 -> P1 (requested)
|
||||||
|
p2.pavoTokens -= this.state.requestPavo; p1.pavoTokens += this.state.requestPavo;
|
||||||
|
p2.eloteTokens -= this.state.requestElote; p1.eloteTokens += this.state.requestElote;
|
||||||
|
}
|
||||||
|
this.clearOffer();
|
||||||
}
|
}
|
||||||
|
else if (p2Action === 'reject') {
|
||||||
|
// No changes
|
||||||
|
this.clearOffer();
|
||||||
|
}
|
||||||
|
else if (p2Action === 'snatch') {
|
||||||
|
// Transfer only offered P1 -> P2
|
||||||
|
if (p1.pavoTokens >= this.state.offerPavo && p1.eloteTokens >= this.state.offerElote) {
|
||||||
|
p1.pavoTokens -= this.state.offerPavo; p2.pavoTokens += this.state.offerPavo;
|
||||||
|
p1.eloteTokens -= this.state.offerElote; p2.eloteTokens += this.state.offerElote;
|
||||||
|
}
|
||||||
|
// Keep offer data around for potential G4 report; it will be cleared on report or next round
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearOffer() {
|
||||||
|
this.state.offerPavo = 0;
|
||||||
|
this.state.offerElote = 0;
|
||||||
|
this.state.requestPavo = 0;
|
||||||
|
this.state.requestElote = 0;
|
||||||
|
this.state.offerActive = false;
|
||||||
|
this.state.p1Action = "";
|
||||||
|
this.state.p2Action = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleRestart() {
|
private handleRestart() {
|
||||||
@@ -161,7 +295,7 @@ export class GameRoom extends Room<GameState> {
|
|||||||
this.broadcast("gameRestart");
|
this.broadcast("gameRestart");
|
||||||
|
|
||||||
if (this.state.players.size === 2) {
|
if (this.state.players.size === 2) {
|
||||||
setTimeout(() => this.startGame(), 2000);
|
setTimeout(() => this.startGame(), 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,11 +322,40 @@ export class GameRoom extends Room<GameState> {
|
|||||||
players: Array.from(this.state.players.values()).map(p => ({
|
players: Array.from(this.state.players.values()).map(p => ({
|
||||||
sessionId: p.sessionId,
|
sessionId: p.sessionId,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
clicks: p.clicks
|
role: p.role,
|
||||||
|
pavoTokens: p.pavoTokens,
|
||||||
|
eloteTokens: p.eloteTokens,
|
||||||
|
shameTokens: p.shameTokens,
|
||||||
})),
|
})),
|
||||||
gameStatus: this.state.gameStatus,
|
gameStatus: this.state.gameStatus,
|
||||||
timeRemaining: this.state.timeRemaining,
|
variant: this.state.currentVariant,
|
||||||
winner: this.state.winner
|
round: this.state.currentRound,
|
||||||
|
decisions: {
|
||||||
|
p1Action: this.state.p1Action,
|
||||||
|
p2Action: this.state.p2Action,
|
||||||
|
forcedByP2: this.state.forcedByP2,
|
||||||
|
reported: this.state.reported,
|
||||||
|
shameAssigned: this.state.shameAssigned,
|
||||||
|
offer: {
|
||||||
|
offerPavo: this.state.offerPavo,
|
||||||
|
offerElote: this.state.offerElote,
|
||||||
|
requestPavo: this.state.requestPavo,
|
||||||
|
requestElote: this.state.requestElote,
|
||||||
|
active: this.state.offerActive,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
outcome: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private advanceRound() {
|
||||||
|
if (this.state.currentRound < 3) {
|
||||||
|
this.state.currentRound += 1;
|
||||||
|
this.state.resetRound();
|
||||||
|
this.broadcast("roundStarted", { round: this.state.currentRound });
|
||||||
|
} else {
|
||||||
|
this.state.finishGame();
|
||||||
|
this.endGame();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,17 +5,47 @@ import { GameStatus } from "../../../../shared/types";
|
|||||||
export class GameState extends Schema {
|
export class GameState extends Schema {
|
||||||
@type({ map: Player }) players = new MapSchema<Player>();
|
@type({ map: Player }) players = new MapSchema<Player>();
|
||||||
@type("string") gameStatus: GameStatus = GameStatus.WAITING;
|
@type("string") gameStatus: GameStatus = GameStatus.WAITING;
|
||||||
@type("number") timeRemaining: number = 600; // 10 minutes in seconds
|
@type("number") timeRemaining: number = 0;
|
||||||
@type("string") winner: string = "";
|
@type("string") winner: string = "";
|
||||||
@type("number") startTime: number = 0;
|
@type("number") startTime: number = 0;
|
||||||
@type("string") roomId: string = "";
|
@type("string") roomId: string = "";
|
||||||
|
|
||||||
|
// Roles
|
||||||
|
@type("string") p1Id: string = "";
|
||||||
|
@type("string") p2Id: string = "";
|
||||||
|
|
||||||
|
// Variant & round
|
||||||
|
@type("string") currentVariant: string = "G1"; // G1..G5
|
||||||
|
@type("number") currentRound: number = 1; // 1..3
|
||||||
|
|
||||||
|
// Decisions & flags for current round
|
||||||
|
@type("string") p1Action: string = ""; // no_offer|"" (variable offers handled via fields below)
|
||||||
|
@type("string") p2Action: string = ""; // accept|reject|snatch
|
||||||
|
@type("boolean") forcedByP2: boolean = false; // G2
|
||||||
|
@type("boolean") reported: boolean = false; // G4
|
||||||
|
@type("boolean") shameAssigned: boolean = false; // G3
|
||||||
|
|
||||||
|
// Offer payload (P1 -> P2) and requested return (P2 -> P1)
|
||||||
|
@type("number") offerPavo: number = 0;
|
||||||
|
@type("number") offerElote: number = 0;
|
||||||
|
@type("number") requestPavo: number = 0;
|
||||||
|
@type("number") requestElote: number = 0;
|
||||||
|
@type("boolean") offerActive: boolean = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
addPlayer(sessionId: string, name: string): Player {
|
addPlayer(sessionId: string, name: string): Player {
|
||||||
const player = new Player(sessionId, name);
|
const player = new Player(sessionId, name);
|
||||||
|
// Assign roles P1/P2 in join order
|
||||||
|
if (!this.p1Id) {
|
||||||
|
this.p1Id = sessionId;
|
||||||
|
player.role = "P1";
|
||||||
|
} else if (!this.p2Id) {
|
||||||
|
this.p2Id = sessionId;
|
||||||
|
player.role = "P2";
|
||||||
|
}
|
||||||
this.players.set(sessionId, player);
|
this.players.set(sessionId, player);
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
@@ -27,8 +57,18 @@ export class GameState extends Schema {
|
|||||||
startGame(): void {
|
startGame(): void {
|
||||||
this.gameStatus = GameStatus.PLAYING;
|
this.gameStatus = GameStatus.PLAYING;
|
||||||
this.startTime = Date.now();
|
this.startTime = Date.now();
|
||||||
this.timeRemaining = 600;
|
this.timeRemaining = 0;
|
||||||
this.resetAllPlayers();
|
this.resetAllPlayers();
|
||||||
|
// Initialize tokens by role
|
||||||
|
if (this.p1Id) {
|
||||||
|
const p1 = this.players.get(this.p1Id);
|
||||||
|
if (p1) { p1.pavoTokens = 10; p1.eloteTokens = 0; }
|
||||||
|
}
|
||||||
|
if (this.p2Id) {
|
||||||
|
const p2 = this.players.get(this.p2Id);
|
||||||
|
if (p2) { p2.eloteTokens = 10; p2.pavoTokens = 0; }
|
||||||
|
}
|
||||||
|
this.resetRound();
|
||||||
}
|
}
|
||||||
|
|
||||||
pauseGame(): void {
|
pauseGame(): void {
|
||||||
@@ -45,14 +85,19 @@ export class GameState extends Schema {
|
|||||||
|
|
||||||
finishGame(): void {
|
finishGame(): void {
|
||||||
this.gameStatus = GameStatus.FINISHED;
|
this.gameStatus = GameStatus.FINISHED;
|
||||||
this.determineWinner();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
restartGame(): void {
|
restartGame(): void {
|
||||||
this.gameStatus = GameStatus.WAITING;
|
this.gameStatus = GameStatus.WAITING;
|
||||||
this.timeRemaining = 600;
|
this.timeRemaining = 0;
|
||||||
this.winner = "";
|
this.winner = "";
|
||||||
this.startTime = 0;
|
this.startTime = 0;
|
||||||
|
this.currentRound = 1;
|
||||||
|
this.p1Action = this.p2Action = "";
|
||||||
|
this.forcedByP2 = this.reported = this.shameAssigned = false;
|
||||||
|
this.offerPavo = this.offerElote = 0;
|
||||||
|
this.requestPavo = this.requestElote = 0;
|
||||||
|
this.offerActive = false;
|
||||||
this.resetAllPlayers();
|
this.resetAllPlayers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,27 +105,14 @@ export class GameState extends Schema {
|
|||||||
this.players.forEach(player => player.reset());
|
this.players.forEach(player => player.reset());
|
||||||
}
|
}
|
||||||
|
|
||||||
private determineWinner(): void {
|
resetRound(): void {
|
||||||
let maxClicks = -1;
|
this.p1Action = "";
|
||||||
let winner = "";
|
this.p2Action = "";
|
||||||
|
this.forcedByP2 = (this.currentVariant === "G2");
|
||||||
this.players.forEach(player => {
|
this.reported = false;
|
||||||
if (player.clicks > maxClicks) {
|
this.shameAssigned = false;
|
||||||
maxClicks = player.clicks;
|
this.offerPavo = this.offerElote = 0;
|
||||||
winner = player.name;
|
this.requestPavo = this.requestElote = 0;
|
||||||
}
|
this.offerActive = false;
|
||||||
});
|
|
||||||
|
|
||||||
this.winner = winner;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTimer(deltaTime: number): void {
|
|
||||||
if (this.gameStatus === GameStatus.PLAYING && this.timeRemaining > 0) {
|
|
||||||
this.timeRemaining -= deltaTime;
|
|
||||||
if (this.timeRemaining <= 0) {
|
|
||||||
this.timeRemaining = 0;
|
|
||||||
this.finishGame();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,10 @@ export class Player extends Schema {
|
|||||||
@type("string") name: string = "";
|
@type("string") name: string = "";
|
||||||
@type("number") clicks: number = 0;
|
@type("number") clicks: number = 0;
|
||||||
@type("boolean") connected: boolean = true;
|
@type("boolean") connected: boolean = true;
|
||||||
|
@type("string") role: string = ""; // 'P1' | 'P2'
|
||||||
|
@type("number") pavoTokens: number = 0;
|
||||||
|
@type("number") eloteTokens: number = 0;
|
||||||
|
@type("number") shameTokens: number = 0;
|
||||||
|
|
||||||
constructor(sessionId: string, name: string) {
|
constructor(sessionId: string, name: string) {
|
||||||
super();
|
super();
|
||||||
@@ -12,6 +16,10 @@ export class Player extends Schema {
|
|||||||
this.name = name;
|
this.name = name;
|
||||||
this.clicks = 0;
|
this.clicks = 0;
|
||||||
this.connected = true;
|
this.connected = true;
|
||||||
|
this.role = "";
|
||||||
|
this.pavoTokens = 0;
|
||||||
|
this.eloteTokens = 0;
|
||||||
|
this.shameTokens = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
incrementClicks(): void {
|
incrementClicks(): void {
|
||||||
@@ -20,5 +28,7 @@ export class Player extends Schema {
|
|||||||
|
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.clicks = 0;
|
this.clicks = 0;
|
||||||
|
this.pavoTokens = 0;
|
||||||
|
this.eloteTokens = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user