diff --git a/public/css/1v1.css b/public/css/1v1.css
index 025610f..febcb0c 100644
--- a/public/css/1v1.css
+++ b/public/css/1v1.css
@@ -1196,3 +1196,88 @@ body {
0 0 calc(var(--s) * 42) rgba(220, 60, 60, 0.75),
inset 0 0 calc(var(--s) * 20) rgba(0, 0, 0, 0.5) !important;
}
+
+/* ══════════════════════════════════════════════════════
+ ABLAGESTAPEL (Discard Zone)
+══════════════════════════════════════════════════════ */
+.hand-slot-discard {
+ position: relative;
+ width: calc(var(--s) * 105);
+ height: calc(var(--s) * 160);
+ border-radius: calc(var(--s) * 9);
+ border: 2px dashed rgba(180, 80, 60, 0.4);
+ background: rgba(20, 8, 5, 0.75);
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: default;
+ transition: border-color 0.2s, background 0.2s;
+ box-shadow: 0 calc(var(--s) * 4) calc(var(--s) * 14) rgba(0,0,0,0.35);
+ margin-right: calc(var(--s) * 8);
+}
+.hand-slot-discard.drag-over {
+ border-color: rgba(220, 60, 60, 0.95);
+ background: rgba(80, 15, 10, 0.7);
+ box-shadow: 0 0 calc(var(--s) * 20) rgba(220,60,60,0.4);
+}
+
+.discard-stack-wrap {
+ position: absolute;
+ inset: calc(var(--s) * 8) calc(var(--s) * 8) calc(var(--s) * 20);
+}
+
+/* Gestapelte Karten-Rückseiten im Ablagestapel */
+.discard-card-back {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: calc(var(--s) * 7);
+ filter: grayscale(70%) brightness(0.55);
+ box-shadow: 0 2px 6px rgba(0,0,0,0.6);
+}
+.discard-card-back:nth-child(1) { transform: translate(-3px, -3px) rotate(-4deg); }
+.discard-card-back:nth-child(2) { transform: translate(-1px, -1px) rotate(-1.5deg); }
+.discard-card-back:nth-child(3) { transform: translate(0, 0) rotate(1deg); }
+
+.discard-label {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-family: "Cinzel", serif;
+ font-size: calc(var(--s) * 9);
+ color: rgba(200, 100, 80, 0.6);
+ letter-spacing: calc(var(--s) * 1);
+ pointer-events: none;
+ white-space: nowrap;
+ text-align: center;
+}
+.discard-stack-wrap:not(:empty) + .discard-label {
+ display: none; /* Label verstecken wenn Karten drin */
+}
+
+.discard-count {
+ position: absolute;
+ bottom: calc(var(--s) * 3);
+ left: 0; right: 0;
+ text-align: center;
+ font-family: "Cinzel", serif;
+ font-size: calc(var(--s) * 10);
+ color: rgba(200, 100, 80, 0.75);
+ text-shadow: 0 1px 3px rgba(0,0,0,0.8);
+}
+.discard-count:empty,
+.discard-count[data-count="0"] { opacity: 0; }
+
+/* Board-Karte draggable während eigenem Zug */
+.board-slot-draggable {
+ cursor: grab !important;
+}
+.board-slot-draggable:active {
+ cursor: grabbing !important;
+}
+.board-slot-draggable.dragging {
+ opacity: 0.45;
+}
diff --git a/public/js/buildings/1v1.js b/public/js/buildings/1v1.js
index 4355902..116fcaf 100644
--- a/public/js/buildings/1v1.js
+++ b/public/js/buildings/1v1.js
@@ -233,6 +233,7 @@ function updateTimerUI(secs, activeName) {
═══════════════════════════════════════════════════════════ */
function setTurnState(myTurn, activeName) {
isMyTurn = myTurn;
+ if (myTurn) enableBoardDrag(); else disableBoardDrag();
const btn = document.getElementById('end-turn-btn');
if (!btn) return;
handCardIds.forEach(id => renderHandSlot(id));
@@ -652,6 +653,130 @@ document.getElementById('handArea').addEventListener('dragend', e => {
});
});
+
+
+/* ═══════════════════════════════════════════════════════════
+ ABLAGESTAPEL – DISCARD SYSTEM
+ Spieler zieht Karte vom Board → auf Discard-Zone droppen
+ → Karte weg vom Board, ausgegraut auf Stapel, nicht mehr spielbar
+═══════════════════════════════════════════════════════════ */
+
+const discardPile = []; // abgeworfene Karten (lokal)
+
+/* ── Discard-Zone Drag-Events ────────────────────────────── */
+const discardZone = document.getElementById('discard-zone');
+
+discardZone.addEventListener('dragover', e => {
+ if (!isMyTurn) return;
+ if (!draggedBoardSlotId) return; // nur Board-Karten
+ e.preventDefault();
+ discardZone.classList.add('drag-over');
+});
+
+discardZone.addEventListener('dragleave', () => {
+ discardZone.classList.remove('drag-over');
+});
+
+discardZone.addEventListener('drop', e => {
+ e.preventDefault();
+ discardZone.classList.remove('drag-over');
+ if (!draggedBoardSlotId || !isMyTurn) return;
+ discardBoardCard(draggedBoardSlotId);
+ draggedBoardSlotId = null;
+});
+
+/* ── Board-Karten draggable machen / entfernen ───────────── */
+let draggedBoardSlotId = null;
+
+function enableBoardDrag() {
+ document.querySelectorAll('.card-slot').forEach(slot => {
+ const idx = Number(slot.dataset.slotIndex);
+ if (!isMyZone(idx)) return;
+ if (!boardState[slot.id]) return;
+ if (boardState[slot.id].owner !== mySlot) return;
+
+ slot.classList.add('board-slot-draggable');
+ slot.setAttribute('draggable', 'true');
+
+ // Event-Listener nur einmal anhängen
+ if (!slot.dataset.discardListenerAttached) {
+ slot.dataset.discardListenerAttached = '1';
+ slot.addEventListener('dragstart', e => {
+ if (!isMyTurn) { e.preventDefault(); return; }
+ draggedBoardSlotId = slot.id;
+ slot.classList.add('dragging');
+ e.dataTransfer.effectAllowed = 'move';
+ e.dataTransfer.setData('text/board', slot.id);
+ });
+ slot.addEventListener('dragend', () => {
+ slot.classList.remove('dragging');
+ draggedBoardSlotId = null;
+ discardZone.classList.remove('drag-over');
+ });
+ }
+ });
+}
+
+function disableBoardDrag() {
+ document.querySelectorAll('.board-slot-draggable').forEach(slot => {
+ slot.classList.remove('board-slot-draggable');
+ slot.setAttribute('draggable', 'false');
+ });
+}
+
+/* ── Karte abwerfen ──────────────────────────────────────── */
+function discardBoardCard(slotId) {
+ const entry = boardState[slotId];
+ if (!entry || entry.owner !== mySlot) return;
+
+ const card = entry.card;
+ discardPile.push(card);
+ delete boardState[slotId];
+
+ // Slot leeren
+ const slotEl = document.getElementById(slotId);
+ if (slotEl) {
+ slotEl.innerHTML = '';
+ slotEl.classList.remove('board-slot-draggable', 'dragging');
+ slotEl.setAttribute('draggable', 'false');
+ delete slotEl.dataset.discardListenerAttached;
+ }
+
+ updateDiscardZone();
+
+ socket.emit('discard_card', { matchId, slotId, slot: mySlot, cardId: card.id });
+ console.log(`[Discard] Karte abgeworfen: ${card.name} von ${slotId}`);
+}
+
+/* ── Discard-Zone visuell updaten ────────────────────────── */
+function updateDiscardZone() {
+ const stack = document.getElementById('discard-stack');
+ const count = document.getElementById('discard-count');
+ if (!stack) return;
+
+ const shown = Math.min(discardPile.length, 3);
+ stack.innerHTML = Array.from({ length: shown }).map(() =>
+ `
`
+ ).join('');
+
+ if (count) {
+ count.textContent = discardPile.length > 0 ? discardPile.length : '';
+ count.dataset.count = discardPile.length;
+ }
+}
+
+/* ── Gegner wirft Karte ab → Slot leeren ─────────────────── */
+socket.on('card_discarded', data => {
+ const { slotId } = data;
+ if (!slotId) return;
+ const slotEl = document.getElementById(slotId);
+ if (slotEl) slotEl.innerHTML = '';
+ delete boardState[slotId];
+ console.log(`[Discard] Gegner hat Karte abgeworfen von ${slotId}`);
+});
+
+/* ── Board-Drag bei Zugwechsel aktivieren/deaktivieren ────── */
+// Wird von setTurnState aufgerufen (siehe unten)
socket.on('card_played', data => {
if (data.slot === mySlot) return;
if (boardState[data.boardSlot]) return;
diff --git a/sockets/1vKI_daily.socket.js b/sockets/1vKI_daily.socket.js
index b909986..dfccaea 100644
--- a/sockets/1vKI_daily.socket.js
+++ b/sockets/1vKI_daily.socket.js
@@ -153,14 +153,23 @@ async function askHaiku(boardState, aiHand, availableSlots, aiSlot, leftSlot) {
defends: c.defends ?? 0, race: c.race ?? 0, range: c.range ?? 1,
}));
+ // KI-eigene Karten auf dem Board (könnten abgeworfen werden)
+ const myBoardCards = Object.entries(boardState)
+ .filter(([, e]) => e.owner === aiSlot)
+ .map(([slot, e]) => ({ slot, name: e.card.name, atk: e.card.attack ?? 0 }));
+
const prompt = `Kartenspiel. Du bist die KI (${isLeft ? 'linke Seite, Slots 1-3, bewegst dich nach rechts' : 'rechte Seite, Slots 9-11, bewegst dich nach links'}).
Board: ${JSON.stringify(boardSummary)}
+Deine eigenen Karten auf dem Board: ${JSON.stringify(myBoardCards)}
Deine Hand: ${JSON.stringify(handSummary)}
Freie Slots: ${JSON.stringify(availableSlots)}
-Wähle eine Karte und einen freien Slot. Bevorzuge Karten mit hohem Angriff oder hoher Bewegung (race).
-Antworte NUR mit validem JSON: {"card_index":0,"slot":"row1-slot-9"}
-Wenn keine Karte oder kein Slot: {"skip":true}`;
+Optionen:
+1. Karte ausspielen: {"card_index":0,"slot":"row1-slot-9"}
+2. Eigene Boardkarte abwerfen (wenn sie schlecht positioniert ist): {"discard_slot":"row1-slot-9"}
+3. Nichts tun: {"skip":true}
+
+Antworte NUR mit validem JSON.`;
try {
const res = await fetch('https://api.anthropic.com/v1/messages', {
@@ -213,7 +222,20 @@ async function playAiTurn(io, matchId) {
aiRoom.boardState, readyCards, freeSlots, aiRoom.aiSlot, aiRoom.leftSlot
);
- if (decision && !decision.skip && decision.card_index != null && decision.slot) {
+ // KI wirft eigene Boardkarte ab
+ if (decision && decision.discard_slot) {
+ const dsSlot = decision.discard_slot;
+ if (aiRoom.boardState[dsSlot]?.owner === aiRoom.aiSlot) {
+ delete aiRoom.boardState[dsSlot];
+ const ioRoom = io._arenaRooms?.get(matchId);
+ if (ioRoom?.boardState) delete ioRoom.boardState[dsSlot];
+ io.to(aiRoom.playerSocketId).emit('card_discarded', { slotId: dsSlot });
+ console.log(`[HT] KI wirft ab: ${dsSlot}`);
+ await sleep(400);
+ }
+ }
+
+ if (decision && !decision.skip && !decision.discard_slot && decision.card_index != null && decision.slot) {
const card = readyCards[decision.card_index];
const slotId = decision.slot;
@@ -514,6 +536,23 @@ function registerHimmelstorHandlers(io, socket) {
console.log(`[HT] arena_join KI-Match: ${matchId}`);
});
+
+ /* ── Spieler wirft Karte ab (KI-Match) ───────────────────── */
+ socket.on('discard_card', (data) => {
+ const { matchId, slotId, slot } = data;
+ if (!matchId || !htAiMatchIds.has(matchId)) return;
+
+ const aiRoom = htAiRooms.get(matchId);
+ if (!aiRoom || aiRoom.gameOver) return;
+ if (slot !== aiRoom.playerSlot) return; // nur eigene Karten
+
+ if (aiRoom.boardState) delete aiRoom.boardState[slotId];
+ const ioRoom = io._arenaRooms?.get(matchId);
+ if (ioRoom?.boardState) delete ioRoom.boardState[slotId];
+
+ console.log(`[HT] Spieler wirft ab: ${slotId} | Match ${matchId}`);
+ });
+
/* ── end_turn für KI-Matches ────────────────────────────── */
socket.on('end_turn', async (data) => {
const { matchId, slot } = data;
diff --git a/sockets/arena.socket.js b/sockets/arena.socket.js
index 76d214b..1692028 100644
--- a/sockets/arena.socket.js
+++ b/sockets/arena.socket.js
@@ -817,6 +817,30 @@ function registerArenaHandlers(io, socket) {
});
/* ── Zug beenden → Kampfphase → Zugwechsel ── */
+
+ /* ── Karte vom Board abwerfen ────────────────────────────── */
+ socket.on('discard_card', (data) => {
+ const { matchId, slotId, slot } = data;
+ if (!matchId || !slotId) return;
+ if (htAiMatchIds.has(matchId)) return; // KI-Matches: handled by himmelstor.socket
+
+ const room = io._arenaRooms?.get(matchId);
+ if (!room || room.gameOver) return;
+ if (room.sockets[slot] !== socket.id) return; // nur eigene Karten
+
+ // Karte aus boardState entfernen
+ if (room.boardState) delete room.boardState[slotId];
+ room.boardCards = room.boardCards?.filter(c => c.boardSlot !== slotId) ?? [];
+
+ // Gegner informieren
+ const oppSlot = slot === 'player1' ? 'player2' : 'player1';
+ const oppSocket = room.sockets[oppSlot];
+ if (oppSocket) {
+ io.to(oppSocket).emit('card_discarded', { slotId });
+ }
+ console.log(`[Arena] Karte abgeworfen: ${slotId} von ${slot} | Match ${matchId}`);
+ });
+
socket.on("end_turn", async (data) => {
const { matchId, slot } = data;
if (!matchId || !slot) return;
diff --git a/views/1v1-battlefield.ejs b/views/1v1-battlefield.ejs
index e8ea391..67bcf01 100644
--- a/views/1v1-battlefield.ejs
+++ b/views/1v1-battlefield.ejs
@@ -89,6 +89,12 @@