|
|
|
@ -54,7 +54,14 @@ |
|
|
|
|
|
|
|
<view class="tips"> |
|
|
|
<text>规则:点选未被压住的卡牌,底部槽内3张相同自动消除,槽满则失败。</text> |
|
|
|
<text>地狱难度 · 剩余 {{remainingCount}} 张 · 当前可点 {{availableCount}} 张 · {{riskText}}</text> |
|
|
|
<text>陷阱难度 · 剩余 {{remainingCount}} 张 · 当前可点 {{availableCount}} 张 · {{riskText}}</text> |
|
|
|
</view> |
|
|
|
|
|
|
|
<view class="debug-panel"> |
|
|
|
<view><text>{{availableCount}}</text><text>可点</text></view> |
|
|
|
<view><text>{{slots.length}}/{{slotLimit}}</text><text>槽位</text></view> |
|
|
|
<view><text>{{directComboCount}}</text><text>可消</text></view> |
|
|
|
<view><text>{{trapGroupCount}}</text><text>陷阱组</text></view> |
|
|
|
</view> |
|
|
|
|
|
|
|
<view class="board"> |
|
|
|
@ -139,7 +146,7 @@ |
|
|
|
startTs: 0, |
|
|
|
cards: [], |
|
|
|
slots: [], |
|
|
|
slotLimit: 5, |
|
|
|
slotLimit: 7, |
|
|
|
moveCount: 0, |
|
|
|
modal: { show: false, title: '', sub: '' } |
|
|
|
} |
|
|
|
@ -181,6 +188,22 @@ |
|
|
|
availableCount() { |
|
|
|
return this.cards.filter(item => !item.removed && !item.selected && !this.isLocked(item)).length |
|
|
|
}, |
|
|
|
directComboCount() { |
|
|
|
const map = {} |
|
|
|
this.cards.forEach(item => { |
|
|
|
if (!item.removed && !item.selected && !this.isLocked(item)) { |
|
|
|
map[item.icon] = (map[item.icon] || 0) + 1 |
|
|
|
} |
|
|
|
}) |
|
|
|
return Object.keys(map).filter(k => map[k] >= 3).length |
|
|
|
}, |
|
|
|
trapGroupCount() { |
|
|
|
const map = {} |
|
|
|
this.cards.forEach(item => { |
|
|
|
if (item.trap) map[item.icon] = true |
|
|
|
}) |
|
|
|
return Object.keys(map).length |
|
|
|
}, |
|
|
|
riskText() { |
|
|
|
if (this.slots.length >= this.slotLimit - 1) return '危险:差1格就满' |
|
|
|
if (this.slots.length >= this.slotLimit - 2) return '注意槽位' |
|
|
|
@ -256,12 +279,12 @@ |
|
|
|
}) |
|
|
|
}, |
|
|
|
buildLevel() { |
|
|
|
const icons = ['🍔', '🥤', '📚', '🏀', '🎧', '🚲', '🍜', '📦', '☕', '🧋', '🎮', '🌟', '🍟', '🥪'] |
|
|
|
const icons = ['🍔', '🥤', '📚', '🏀', '🎧', '🚲', '🍜', '📦', '☕', '🧋'] |
|
|
|
let cards = [] |
|
|
|
for (let attempt = 0; attempt < 8; attempt++) { |
|
|
|
const seed = this.levelSeed() + '_try_' + attempt |
|
|
|
const positions = this.buildCardPositions(seed) |
|
|
|
cards = this.assignSolvableIcons(positions, icons, seed) |
|
|
|
cards = this.assignTrapLevelIcons(positions, icons, seed) |
|
|
|
if (cards.length) break |
|
|
|
} |
|
|
|
if (!cards.length) { |
|
|
|
@ -273,25 +296,29 @@ |
|
|
|
}, |
|
|
|
buildCardPositions(seed) { |
|
|
|
const cards = [] |
|
|
|
const layerCounts = [42, 36, 30, 18] |
|
|
|
const template = this.levelTemplate('hell') |
|
|
|
const layerCounts = template.layerCounts |
|
|
|
const rand = this.seededRandom(seed + '_pos') |
|
|
|
let idx = 0 |
|
|
|
layerCounts.forEach((count, layer) => { |
|
|
|
for (let i = 0; i < count; i++) { |
|
|
|
const col = i % 7 |
|
|
|
const row = Math.floor(i / 7) |
|
|
|
const zone = template.zones[layer] |
|
|
|
const col = i % zone.cols |
|
|
|
const row = Math.floor(i / zone.cols) |
|
|
|
const dx = Math.floor(rand() * 18) |
|
|
|
const dy = Math.floor(rand() * 14) |
|
|
|
const card = { |
|
|
|
id: 'c' + idx, |
|
|
|
icon: '', |
|
|
|
layer, |
|
|
|
x: 12 + col * 70 + (layer * 16) + ((row % 2) * 10) + dx, |
|
|
|
y: 16 + row * 62 + (layer * 34) + dy, |
|
|
|
x: zone.x + col * zone.gapX + (layer * zone.offsetX) + ((row % 2) * zone.stagger) + dx, |
|
|
|
y: zone.y + row * zone.gapY + (layer * zone.offsetY) + dy, |
|
|
|
style: '', |
|
|
|
removed: false, |
|
|
|
selected: false, |
|
|
|
solutionStep: 0 |
|
|
|
solutionStep: 0, |
|
|
|
trap: false, |
|
|
|
keyCard: false |
|
|
|
} |
|
|
|
card.style = `left:${card.x}rpx;top:${card.y}rpx;z-index:${card.layer + 1};` |
|
|
|
cards.push(card) |
|
|
|
@ -300,6 +327,43 @@ |
|
|
|
}) |
|
|
|
return cards |
|
|
|
}, |
|
|
|
levelTemplate(type) { |
|
|
|
const templates = { |
|
|
|
simple: { |
|
|
|
layerCounts: [36, 30, 24], |
|
|
|
zones: [ |
|
|
|
{ x: 20, y: 18, cols: 6, gapX: 82, gapY: 72, offsetX: 8, offsetY: 18, stagger: 8 }, |
|
|
|
{ x: 70, y: 72, cols: 5, gapX: 78, gapY: 70, offsetX: 10, offsetY: 22, stagger: 10 }, |
|
|
|
{ x: 96, y: 110, cols: 5, gapX: 74, gapY: 66, offsetX: 12, offsetY: 24, stagger: 10 } |
|
|
|
] |
|
|
|
}, |
|
|
|
normal: { |
|
|
|
layerCounts: [36, 30, 24], |
|
|
|
zones: [ |
|
|
|
{ x: 18, y: 18, cols: 6, gapX: 80, gapY: 70, offsetX: 12, offsetY: 24, stagger: 10 }, |
|
|
|
{ x: 78, y: 76, cols: 5, gapX: 76, gapY: 66, offsetX: 14, offsetY: 28, stagger: 12 }, |
|
|
|
{ x: 110, y: 116, cols: 4, gapX: 74, gapY: 62, offsetX: 16, offsetY: 30, stagger: 12 } |
|
|
|
] |
|
|
|
}, |
|
|
|
difficult: { |
|
|
|
layerCounts: [36, 30, 24], |
|
|
|
zones: [ |
|
|
|
{ x: 14, y: 16, cols: 6, gapX: 80, gapY: 70, offsetX: 14, offsetY: 28, stagger: 12 }, |
|
|
|
{ x: 92, y: 76, cols: 5, gapX: 70, gapY: 62, offsetX: 18, offsetY: 32, stagger: 14 }, |
|
|
|
{ x: 132, y: 122, cols: 4, gapX: 66, gapY: 58, offsetX: 20, offsetY: 34, stagger: 14 } |
|
|
|
] |
|
|
|
}, |
|
|
|
hell: { |
|
|
|
layerCounts: [36, 30, 24], |
|
|
|
zones: [ |
|
|
|
{ x: 12, y: 16, cols: 6, gapX: 82, gapY: 72, offsetX: 10, offsetY: 22, stagger: 10 }, |
|
|
|
{ x: 86, y: 72, cols: 5, gapX: 70, gapY: 60, offsetX: 18, offsetY: 34, stagger: 16 }, |
|
|
|
{ x: 126, y: 112, cols: 4, gapX: 64, gapY: 54, offsetX: 22, offsetY: 38, stagger: 16 } |
|
|
|
] |
|
|
|
} |
|
|
|
} |
|
|
|
return templates[type] || templates.hell |
|
|
|
}, |
|
|
|
assignSolvableIcons(positions, icons, seed) { |
|
|
|
// 上线关卡不能随机发牌。这里先模拟一条真实可点击的消除路径, |
|
|
|
// 再按路径给三张牌分配同一图案,保证每日关卡至少存在一条解法。 |
|
|
|
@ -330,8 +394,121 @@ |
|
|
|
}) |
|
|
|
return positions |
|
|
|
}, |
|
|
|
assignTrapLevelIcons(positions, icons, seed) { |
|
|
|
const rand = this.seededRandom(seed + '_trap') |
|
|
|
const cards = positions.slice() |
|
|
|
const trapIcons = this.seededShuffle(icons.slice(), seed + '_trap_icons').slice(0, 4) |
|
|
|
const remaining = cards.slice() |
|
|
|
const groups = [] |
|
|
|
const trapPlans = this.buildTrapPlans(cards, trapIcons, seed) |
|
|
|
if (trapPlans.length < 4) return [] |
|
|
|
|
|
|
|
while (remaining.length) { |
|
|
|
const available = remaining.filter(card => !this.isBlockedIn(card, remaining)) |
|
|
|
if (available.length < 3) return [] |
|
|
|
const readyTrap = trapPlans.find(plan => { |
|
|
|
return !plan.used && plan.cards.every(card => remaining.indexOf(card) >= 0) && !this.isBlockedIn(plan.key, remaining) |
|
|
|
}) |
|
|
|
const normalAvailable = available.filter(card => !card.reservedTrap) |
|
|
|
const picked = readyTrap ? readyTrap.cards : this.pickSolutionTriple(normalAvailable, rand) |
|
|
|
if (!picked || picked.length < 3) return [] |
|
|
|
groups.push({ |
|
|
|
cards: picked, |
|
|
|
trapIcon: readyTrap ? readyTrap.icon : '' |
|
|
|
}) |
|
|
|
if (readyTrap) readyTrap.used = true |
|
|
|
picked.forEach(card => { |
|
|
|
const idx = remaining.indexOf(card) |
|
|
|
if (idx >= 0) remaining.splice(idx, 1) |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
if (trapPlans.some(plan => !plan.used)) return [] |
|
|
|
const trapIndexes = [] |
|
|
|
groups.forEach((group, index) => { |
|
|
|
if (group.trapIcon) trapIndexes.push(index) |
|
|
|
}) |
|
|
|
const groupIcons = this.buildPressureIconOrder(groups.length, icons, trapIcons, trapIndexes, seed) |
|
|
|
groups.forEach((group, step) => { |
|
|
|
const icon = group.trapIcon || groupIcons[step] || icons[step % icons.length] |
|
|
|
group.cards.forEach(card => { |
|
|
|
card.icon = icon |
|
|
|
card.solutionStep = step + 1 |
|
|
|
if (group.trapIcon) { |
|
|
|
card.trap = true |
|
|
|
} |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
cards.forEach(card => { |
|
|
|
card.reservedTrap = false |
|
|
|
card.style = `left:${card.x}rpx;top:${card.y}rpx;z-index:${card.layer + 1};` |
|
|
|
}) |
|
|
|
return cards |
|
|
|
}, |
|
|
|
buildTrapPlans(cards, trapIcons, seed) { |
|
|
|
const startAvailable = cards.filter(card => !this.isBlockedIn(card, cards)) |
|
|
|
const baitPool = this.seededShuffle(startAvailable.filter(card => !this.isCenterCard(card)), seed + '_bait') |
|
|
|
const keyPool = this.seededShuffle(cards.filter(card => card.layer === 0 && this.isCenterCard(card)), seed + '_key') |
|
|
|
const plans = [] |
|
|
|
trapIcons.forEach(icon => { |
|
|
|
const baitA = baitPool.shift() |
|
|
|
const baitB = baitPool.shift() |
|
|
|
const key = keyPool.shift() |
|
|
|
if (!baitA || !baitB || !key) return |
|
|
|
;[baitA, baitB, key].forEach(card => { |
|
|
|
card.reservedTrap = true |
|
|
|
card.trap = true |
|
|
|
}) |
|
|
|
key.keyCard = true |
|
|
|
plans.push({ |
|
|
|
icon, |
|
|
|
cards: [baitA, baitB, key], |
|
|
|
key, |
|
|
|
used: false |
|
|
|
}) |
|
|
|
}) |
|
|
|
return plans |
|
|
|
}, |
|
|
|
buildPressureIconOrder(count, icons, trapIcons, trapIndexes, seed) { |
|
|
|
const rand = this.seededRandom(seed + '_pressure') |
|
|
|
const safeIcons = icons.filter(icon => trapIcons.indexOf(icon) < 0) |
|
|
|
const order = new Array(count) |
|
|
|
trapIndexes.forEach((idx, i) => { |
|
|
|
order[idx] = trapIcons[i % trapIcons.length] |
|
|
|
}) |
|
|
|
const usage = {} |
|
|
|
trapIcons.forEach(icon => { |
|
|
|
usage[icon] = 1 |
|
|
|
}) |
|
|
|
for (let i = 0; i < count; i++) { |
|
|
|
if (order[i]) continue |
|
|
|
const pool = i < 12 ? icons : safeIcons |
|
|
|
let icon = pool[i % pool.length] |
|
|
|
let guard = 0 |
|
|
|
while ((usage[icon] || 0) >= 3 && guard < pool.length + icons.length) { |
|
|
|
icon = pool[(i + guard + 1) % pool.length] |
|
|
|
guard++ |
|
|
|
} |
|
|
|
if ((usage[icon] || 0) >= 3) { |
|
|
|
icon = icons.find(item => (usage[item] || 0) < 3) || icons[i % icons.length] |
|
|
|
} |
|
|
|
order[i] = icon |
|
|
|
usage[icon] = (usage[icon] || 0) + 1 |
|
|
|
} |
|
|
|
for (let i = 0; i < order.length; i += 6) { |
|
|
|
const part = order.slice(i, i + 6) |
|
|
|
this.shuffleWithRandom(part, rand) |
|
|
|
for (let j = 0; j < part.length; j++) order[i + j] = part[j] |
|
|
|
} |
|
|
|
return order |
|
|
|
}, |
|
|
|
isCenterCard(card) { |
|
|
|
return card.x >= 160 && card.x <= 400 && card.y >= 120 && card.y <= 440 |
|
|
|
}, |
|
|
|
pickSolutionTriple(available, rand) { |
|
|
|
const pool = available.slice() |
|
|
|
if (pool.length < 3) return [] |
|
|
|
this.shuffleWithRandom(pool, rand) |
|
|
|
const first = pool.shift() |
|
|
|
pool.sort((a, b) => this.cardDistanceScore(b, first) - this.cardDistanceScore(a, first)) |
|
|
|
@ -525,6 +702,10 @@ |
|
|
|
.hero-title { margin-top: 10rpx; font-size: 42rpx; font-weight: 900; } |
|
|
|
.hero-sub, .tips { margin-top: 16rpx; color: #6B817D; font-size: 24rpx; line-height: 1.55; } |
|
|
|
.tips { display: flex; flex-direction: column; gap: 6rpx; } |
|
|
|
.debug-panel { margin-top: 16rpx; display: flex; gap: 10rpx; } |
|
|
|
.debug-panel view { flex: 1; border-radius: 20rpx; padding: 12rpx 6rpx; background: rgba(255,255,255,0.76); text-align: center; border: 1rpx solid rgba(53,214,166,0.16); } |
|
|
|
.debug-panel text:first-child { display: block; color: #22B889; font-size: 28rpx; font-weight: 900; } |
|
|
|
.debug-panel text:last-child { display: block; margin-top: 4rpx; color: #8AA09C; font-size: 20rpx; } |
|
|
|
.college-picker { margin-top: 18rpx; padding: 18rpx 22rpx; border-radius: 26rpx; background: rgba(255,255,255,0.72); display: flex; align-items: center; justify-content: space-between; color: #42635E; font-size: 24rpx; } |
|
|
|
.college-picker text:last-child { color: #22B889; font-weight: 900; } |
|
|
|
.rocket { margin-top: 22rpx; display: flex; align-items: center; gap: 16rpx; } |
|
|
|
@ -553,8 +734,8 @@ |
|
|
|
.slot-wrap { margin-top: 24rpx; } |
|
|
|
.slot-title { color: #6B817D; font-size: 24rpx; margin-bottom: 14rpx; } |
|
|
|
.slots { display: flex; gap: 10rpx; } |
|
|
|
.slot { flex: 1; height: 74rpx; border-radius: 20rpx; background: rgba(255,255,255,0.8); border: 2rpx dashed rgba(53,214,166,0.28); display: flex; align-items: center; justify-content: center; } |
|
|
|
.slot text { font-size: 38rpx; } |
|
|
|
.slot { flex: 1; height: 66rpx; border-radius: 18rpx; background: rgba(255,255,255,0.8); border: 2rpx dashed rgba(53,214,166,0.28); display: flex; align-items: center; justify-content: center; } |
|
|
|
.slot text { font-size: 34rpx; } |
|
|
|
.actions { margin-top: 28rpx; display: flex; gap: 18rpx; } |
|
|
|
.sub-btn, .main-btn { flex: 1; height: 82rpx; line-height: 82rpx; text-align: center; border-radius: 999rpx; font-size: 28rpx; font-weight: 900; } |
|
|
|
.sub-btn { background: rgba(255,255,255,0.78); color: #42635E; } |
|
|
|
|