Removed the unused alignment field from DefensiveDecision model and all related code across backend and frontend. Backend changes: - models/game_models.py: Removed alignment field and validator - terminal_client/display.py: Removed alignment from display - core/ai_opponent.py: Updated log message - tests/unit/models/test_game_models.py: Removed alignment tests - tests/unit/core/test_validators.py: Removed alignment validation test Frontend changes: - types/game.ts: Removed alignment from DefensiveDecision interface - components/Decisions/DefensiveSetup.vue: * Removed alignment section from template * Removed alignment from localSetup initialization * Removed alignmentOptions array * Removed alignmentDisplay computed property * Removed alignment from hasChanges comparison * Removed alignment from visual preview (reorganized to col-span-2) Rationale: Defensive alignment is not active in the game and will not be used. Per Cal's decision, remove completely rather than keep as dead code. Tests: All 728 backend unit tests passing (100%) Session 1 Part 3 - Change #6 complete Part of cleanup work from demo review 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
229 lines
7.1 KiB
Vue
229 lines
7.1 KiB
Vue
<template>
|
||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
|
||
<!-- Header -->
|
||
<div class="flex items-center justify-between mb-6">
|
||
<h3 class="text-xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
||
<span class="text-2xl">🛡️</span>
|
||
Defensive Setup
|
||
</h3>
|
||
<span
|
||
v-if="!isActive"
|
||
class="px-3 py-1 text-xs font-semibold rounded-full bg-gray-200 text-gray-600"
|
||
>
|
||
Opponent's Turn
|
||
</span>
|
||
</div>
|
||
|
||
<!-- Form -->
|
||
<form @submit.prevent="handleSubmit" class="space-y-6">
|
||
<!-- Infield Depth -->
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||
Infield Depth
|
||
</label>
|
||
<ButtonGroup
|
||
v-model="localSetup.infield_depth"
|
||
:options="infieldDepthOptions"
|
||
:disabled="!isActive"
|
||
size="md"
|
||
variant="primary"
|
||
vertical
|
||
/>
|
||
</div>
|
||
|
||
<!-- Outfield Depth -->
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||
Outfield Depth
|
||
</label>
|
||
<ButtonGroup
|
||
v-model="localSetup.outfield_depth"
|
||
:options="outfieldDepthOptions"
|
||
:disabled="!isActive"
|
||
size="md"
|
||
variant="primary"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Hold Runners -->
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||
Hold Runners
|
||
</label>
|
||
<div class="space-y-2">
|
||
<ToggleSwitch
|
||
v-model="holdFirst"
|
||
label="Hold runner at 1st base"
|
||
:disabled="!isActive"
|
||
size="md"
|
||
/>
|
||
<ToggleSwitch
|
||
v-model="holdSecond"
|
||
label="Hold runner at 2nd base"
|
||
:disabled="!isActive"
|
||
size="md"
|
||
/>
|
||
<ToggleSwitch
|
||
v-model="holdThird"
|
||
label="Hold runner at 3rd base"
|
||
:disabled="!isActive"
|
||
size="md"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Visual Preview -->
|
||
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-gray-700 dark:to-gray-600 rounded-lg p-4">
|
||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||
Current Setup
|
||
</h4>
|
||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||
<div>
|
||
<span class="font-medium text-gray-600 dark:text-gray-400">Infield:</span>
|
||
<span class="ml-1 text-gray-900 dark:text-white">{{ infieldDisplay }}</span>
|
||
</div>
|
||
<div>
|
||
<span class="font-medium text-gray-600 dark:text-gray-400">Outfield:</span>
|
||
<span class="ml-1 text-gray-900 dark:text-white">{{ outfieldDisplay }}</span>
|
||
</div>
|
||
<div class="col-span-2">
|
||
<span class="font-medium text-gray-600 dark:text-gray-400">Holding:</span>
|
||
<span class="ml-1 text-gray-900 dark:text-white">{{ holdingDisplay }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Submit Button -->
|
||
<ActionButton
|
||
type="submit"
|
||
variant="success"
|
||
size="lg"
|
||
:disabled="!isActive || !hasChanges"
|
||
:loading="submitting"
|
||
full-width
|
||
>
|
||
{{ submitButtonText }}
|
||
</ActionButton>
|
||
</form>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch } from 'vue'
|
||
import type { DefensiveDecision } from '~/types/game'
|
||
import ButtonGroup from '~/components/UI/ButtonGroup.vue'
|
||
import type { ButtonGroupOption } from '~/components/UI/ButtonGroup.vue'
|
||
import ToggleSwitch from '~/components/UI/ToggleSwitch.vue'
|
||
import ActionButton from '~/components/UI/ActionButton.vue'
|
||
|
||
interface Props {
|
||
gameId: string
|
||
isActive: boolean
|
||
currentSetup?: DefensiveDecision
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
isActive: false,
|
||
})
|
||
|
||
const emit = defineEmits<{
|
||
submit: [setup: DefensiveDecision]
|
||
}>()
|
||
|
||
// Local state
|
||
const submitting = ref(false)
|
||
const localSetup = ref<DefensiveDecision>({
|
||
infield_depth: props.currentSetup?.infield_depth || 'normal',
|
||
outfield_depth: props.currentSetup?.outfield_depth || 'normal',
|
||
hold_runners: props.currentSetup?.hold_runners || [],
|
||
})
|
||
|
||
// Hold runner toggles
|
||
const holdFirst = ref(localSetup.value.hold_runners.includes(1))
|
||
const holdSecond = ref(localSetup.value.hold_runners.includes(2))
|
||
const holdThird = ref(localSetup.value.hold_runners.includes(3))
|
||
|
||
// Watch hold toggles and update hold_runners array
|
||
watch([holdFirst, holdSecond, holdThird], () => {
|
||
const runners: number[] = []
|
||
if (holdFirst.value) runners.push(1)
|
||
if (holdSecond.value) runners.push(2)
|
||
if (holdThird.value) runners.push(3)
|
||
localSetup.value.hold_runners = runners
|
||
})
|
||
|
||
// Options for ButtonGroup components
|
||
const infieldDepthOptions: ButtonGroupOption[] = [
|
||
{ value: 'in', label: 'Infield In', icon: '⬆️' },
|
||
{ value: 'normal', label: 'Normal', icon: '•' },
|
||
{ value: 'back', label: 'Back', icon: '⬇️' },
|
||
{ value: 'double_play', label: 'Double Play', icon: '⚡' },
|
||
{ value: 'corners_in', label: 'Corners In', icon: '◀️▶️' },
|
||
]
|
||
|
||
const outfieldDepthOptions: ButtonGroupOption[] = [
|
||
{ value: 'in', label: 'Shallow', icon: '⬆️' },
|
||
{ value: 'normal', label: 'Normal', icon: '•' },
|
||
{ value: 'back', label: 'Deep', icon: '⬇️' },
|
||
]
|
||
|
||
// Display helpers
|
||
const infieldDisplay = computed(() => {
|
||
const option = infieldDepthOptions.find(opt => opt.value === localSetup.value.infield_depth)
|
||
return option?.label || 'Normal'
|
||
})
|
||
|
||
const outfieldDisplay = computed(() => {
|
||
const option = outfieldDepthOptions.find(opt => opt.value === localSetup.value.outfield_depth)
|
||
return option?.label || 'Normal'
|
||
})
|
||
|
||
const holdingDisplay = computed(() => {
|
||
if (localSetup.value.hold_runners.length === 0) return 'None'
|
||
return localSetup.value.hold_runners.map(base => {
|
||
if (base === 1) return '1st'
|
||
if (base === 2) return '2nd'
|
||
if (base === 3) return '3rd'
|
||
return base
|
||
}).join(', ')
|
||
})
|
||
|
||
// Check if setup has changed from initial
|
||
const hasChanges = computed(() => {
|
||
if (!props.currentSetup) return true
|
||
return (
|
||
localSetup.value.infield_depth !== props.currentSetup.infield_depth ||
|
||
localSetup.value.outfield_depth !== props.currentSetup.outfield_depth ||
|
||
JSON.stringify(localSetup.value.hold_runners) !== JSON.stringify(props.currentSetup.hold_runners)
|
||
)
|
||
})
|
||
|
||
const submitButtonText = computed(() => {
|
||
if (!props.isActive) return 'Wait for Your Turn'
|
||
if (!hasChanges.value) return 'No Changes'
|
||
return 'Submit Defensive Setup'
|
||
})
|
||
|
||
// Handle form submission
|
||
const handleSubmit = async () => {
|
||
if (!props.isActive || !hasChanges.value) return
|
||
|
||
submitting.value = true
|
||
try {
|
||
emit('submit', { ...localSetup.value })
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
// Watch for prop changes and update local state
|
||
watch(() => props.currentSetup, (newSetup) => {
|
||
if (newSetup) {
|
||
localSetup.value = { ...newSetup }
|
||
holdFirst.value = newSetup.hold_runners.includes(1)
|
||
holdSecond.value = newSetup.hold_runners.includes(2)
|
||
holdThird.value = newSetup.hold_runners.includes(3)
|
||
}
|
||
}, { deep: true })
|
||
</script>
|