tejasashinde commited on
Commit
6edd11d
·
verified ·
1 Parent(s): b817c43

feat(gameplay): implement progressive difficulty mechanics and peek cooldown

Browse files
Files changed (1) hide show
  1. app.py +231 -2
app.py CHANGED
@@ -7779,6 +7779,8 @@ class MatchWiseApp {
7779
  this.previewTimer = null;
7780
  this.previewShuffleTimer = null;
7781
  this.previewShuffleAnimationMs = 520;
 
 
7782
  this.previewCount = PREVIEW_SECONDS;
7783
  this.pendingTransition = false;
7784
  this.levelStartTime = null;
@@ -7793,6 +7795,8 @@ class MatchWiseApp {
7793
  this.wrongMatchTimer = null;
7794
  this.distractorSparkleTimer = null;
7795
  this.distractorSparkleClearTimer = null;
 
 
7796
  this.challengeStarted = false;
7797
  this.challengeSelectionLocked = false;
7798
  this.challengeResultIndex = null;
@@ -7948,12 +7952,77 @@ class MatchWiseApp {
7948
  z-index: 4;
7949
  }
7950
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7951
  @keyframes distractorSparkleRuntime {
7952
  0% { transform: scale(0.35) rotate(-18deg); opacity: 0; }
7953
  28% { transform: scale(1.18) rotate(10deg); opacity: 1; }
7954
  100% { transform: scale(0.65) rotate(24deg); opacity: 0; }
7955
  }
7956
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7957
  @keyframes popBounceRuntime {
7958
  0% {
7959
  transform: translate(-50%, -50%) scale(0) rotate(-15deg);
@@ -8776,6 +8845,45 @@ class MatchWiseApp {
8776
  return effectiveMaxHints;
8777
  }
8778
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8779
  getDistractorSparkleIntervalMs() {
8780
  const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
8781
 
@@ -8788,7 +8896,15 @@ class MatchWiseApp {
8788
  return;
8789
  }
8790
 
8791
- const candidates = Array.from(this.grid.querySelectorAll('.memory-card:not(.matched):not(.flipped):not(.preview-locked)'));
 
 
 
 
 
 
 
 
8792
  if (!candidates.length) return;
8793
 
8794
  const card = candidates[Math.floor(Math.random() * candidates.length)];
@@ -8858,6 +8974,7 @@ class MatchWiseApp {
8858
  this.hints <= 0 ||
8859
  this.state.status !== 'playing' ||
8860
  this.hinting ||
 
8861
  oddFaceUp ||
8862
  this.state.game_over ||
8863
  this.state.level_complete;
@@ -9528,6 +9645,99 @@ class MatchWiseApp {
9528
  });
9529
  }
9530
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9531
  startPreviewGridShuffle() {
9532
  this.stopPreviewGridShuffle();
9533
 
@@ -9554,6 +9764,8 @@ class MatchWiseApp {
9554
  this.previewShuffleTimer = null;
9555
  }
9556
 
 
 
9557
  if (!this.grid) return;
9558
 
9559
  this.grid.querySelectorAll('.memory-card').forEach((card) => {
@@ -9609,6 +9821,7 @@ class MatchWiseApp {
9609
  });
9610
  cardEl.style.transform = '';
9611
  cardEl.style.zIndex = '';
 
9612
  });
9613
  }
9614
 
@@ -9660,6 +9873,8 @@ class MatchWiseApp {
9660
  this.state.status = 'preview';
9661
  this.state.level_complete = false;
9662
  this.state.game_over = false;
 
 
9663
  this.syncProgressiveHintLimit(true);
9664
  this.applyStateStyle();
9665
  this.setStatus(
@@ -9688,6 +9903,7 @@ class MatchWiseApp {
9688
 
9689
  setTimeout(() => {
9690
  this.startPreviewGridShuffle();
 
9691
  }, 260);
9692
  this.updateHintsDisplay();
9693
 
@@ -9721,6 +9937,8 @@ class MatchWiseApp {
9721
  this.state.status = 'preview';
9722
  this.state.level_complete = false;
9723
  this.state.game_over = false;
 
 
9724
  this.syncProgressiveHintLimit(true);
9725
 
9726
  if (this.overlay) {
@@ -9752,6 +9970,7 @@ class MatchWiseApp {
9752
 
9753
  setTimeout(() => {
9754
  this.startPreviewGridShuffle();
 
9755
  }, 260);
9756
 
9757
  this.previewTimer = setInterval(() => {
@@ -9938,6 +10157,7 @@ class MatchWiseApp {
9938
 
9939
  card.classList.add('flipped');
9940
  this.flipped.push(idx);
 
9941
 
9942
  if (this.flipped.length === 1) {
9943
  this.updateHintsDisplay();
@@ -9952,6 +10172,7 @@ class MatchWiseApp {
9952
  this.locked = true;
9953
  this.moves += 1;
9954
  this.state.moves = this.moves;
 
9955
  this.updateHud();
9956
 
9957
  const [a, b] = this.flipped;
@@ -10071,6 +10292,7 @@ class MatchWiseApp {
10071
  this.resetComboFeedback();
10072
  this.clearWrongMatchTimer();
10073
  this.locked = true;
 
10074
  this.wrongMatchTimer = window.setTimeout(() => {
10075
  try {
10076
  [a, b].forEach((index) => {
@@ -10087,7 +10309,7 @@ class MatchWiseApp {
10087
  this.gameOver();
10088
  }
10089
  }
10090
- }, 900);
10091
 
10092
  try {
10093
  this.showFeedbackOverlay('WRONG MATCH', 'Lost 1 live', 'negative', false);
@@ -10121,6 +10343,11 @@ class MatchWiseApp {
10121
  if (this.state.status !== 'playing') return;
10122
  this.syncProgressiveHintLimit(false);
10123
  if (this.hinting) return;
 
 
 
 
 
10124
  if (this.hints <= 0) {
10125
  this.setStatus('No peeks left!', 'error', '');
10126
  this.updateHintsDisplay();
@@ -10163,6 +10390,8 @@ class MatchWiseApp {
10163
 
10164
  this.hinting = false;
10165
  this.locked = false;
 
 
10166
  this.updateHintsDisplay();
10167
  await this.syncBackendEvent('hint');
10168
  }, 1200);
 
7779
  this.previewTimer = null;
7780
  this.previewShuffleTimer = null;
7781
  this.previewShuffleAnimationMs = 520;
7782
+ this.previewFogTimers = [];
7783
+ this.previewHideTimers = [];
7784
  this.previewCount = PREVIEW_SECONDS;
7785
  this.pendingTransition = false;
7786
  this.levelStartTime = null;
 
7795
  this.wrongMatchTimer = null;
7796
  this.distractorSparkleTimer = null;
7797
  this.distractorSparkleClearTimer = null;
7798
+ this.recentlyFlippedCardIndexes = [];
7799
+ this.peekCooldownMoves = Number(this.state.peek_cooldown_moves || 0);
7800
  this.challengeStarted = false;
7801
  this.challengeSelectionLocked = false;
7802
  this.challengeResultIndex = null;
 
7952
  z-index: 4;
7953
  }
7954
 
7955
+ .memory-card.distractor-sparkle .memory-back::after {
7956
+ content: '✦';
7957
+ position: absolute;
7958
+ left: 9%;
7959
+ top: 8%;
7960
+ width: 28%;
7961
+ height: 28%;
7962
+ display: flex;
7963
+ align-items: center;
7964
+ justify-content: center;
7965
+ border-radius: 999px;
7966
+ color: rgba(255, 255, 255, 0.95);
7967
+ background: radial-gradient(circle, rgba(255,255,255,0.7), rgba(255,255,255,0.1) 58%, transparent 72%);
7968
+ filter: drop-shadow(0 0 10px rgba(255,255,255,0.75));
7969
+ font-size: clamp(14px, calc(var(--card-fit-size, var(--card-size, 150px)) * 0.16), 26px);
7970
+ pointer-events: none;
7971
+ animation: distractorSparkleRuntime 680ms ease-out forwards;
7972
+ z-index: 4;
7973
+ }
7974
+
7975
+ .memory-container.preview-fog-blink .memory-card.preview-locked .memory-back,
7976
+ .matchwise-root.preview-fog-blink .memory-card.preview-locked .memory-back {
7977
+ animation: previewFogBlinkRuntime 260ms ease-in-out forwards;
7978
+ }
7979
+
7980
+ .memory-card.preview-hidden-card .memory-back {
7981
+ animation: previewCardHideRuntime 620ms ease-in-out forwards;
7982
+ }
7983
+
7984
+ .memory-card.preview-hidden-card::after {
7985
+ content: '☁️';
7986
+ position: absolute;
7987
+ inset: 10%;
7988
+ display: flex;
7989
+ align-items: center;
7990
+ justify-content: center;
7991
+ border-radius: 24px;
7992
+ font-size: clamp(24px, calc(var(--card-fit-size, var(--card-size, 150px)) * 0.28), 46px);
7993
+ background: radial-gradient(circle, rgba(255,255,255,0.95), rgba(255,255,255,0.62) 55%, rgba(255,255,255,0.14));
7994
+ filter: drop-shadow(0 10px 18px rgba(132, 90, 255, 0.2));
7995
+ pointer-events: none;
7996
+ z-index: 8;
7997
+ animation: previewCloudCoverRuntime 620ms ease-in-out forwards;
7998
+ }
7999
+
8000
  @keyframes distractorSparkleRuntime {
8001
  0% { transform: scale(0.35) rotate(-18deg); opacity: 0; }
8002
  28% { transform: scale(1.18) rotate(10deg); opacity: 1; }
8003
  100% { transform: scale(0.65) rotate(24deg); opacity: 0; }
8004
  }
8005
 
8006
+ @keyframes previewFogBlinkRuntime {
8007
+ 0% { filter: blur(0px) brightness(1); opacity: 1; }
8008
+ 45% { filter: blur(5px) brightness(0.94); opacity: 0.72; }
8009
+ 100% { filter: blur(0px) brightness(1); opacity: 1; }
8010
+ }
8011
+
8012
+ @keyframes previewCardHideRuntime {
8013
+ 0% { filter: blur(0px); opacity: 1; }
8014
+ 18% { filter: blur(3px); opacity: 0.18; }
8015
+ 82% { filter: blur(3px); opacity: 0.18; }
8016
+ 100% { filter: blur(0px); opacity: 1; }
8017
+ }
8018
+
8019
+ @keyframes previewCloudCoverRuntime {
8020
+ 0% { transform: scale(0.86); opacity: 0; }
8021
+ 18% { transform: scale(1); opacity: 1; }
8022
+ 82% { transform: scale(1); opacity: 1; }
8023
+ 100% { transform: scale(1.06); opacity: 0; }
8024
+ }
8025
+
8026
  @keyframes popBounceRuntime {
8027
  0% {
8028
  transform: translate(-50%, -50%) scale(0) rotate(-15deg);
 
8845
  return effectiveMaxHints;
8846
  }
8847
 
8848
+ getHintCooldownLengthMoves() {
8849
+ const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
8850
+
8851
+ // Early levels stay friendly. Later levels make Peek a small decision.
8852
+ if (level < 7) return 0;
8853
+ if (level < 14) return 1;
8854
+ return 2;
8855
+ }
8856
+
8857
+ decrementPeekCooldownAfterMove() {
8858
+ if (this.peekCooldownMoves > 0) {
8859
+ this.peekCooldownMoves = Math.max(0, this.peekCooldownMoves - 1);
8860
+ this.state.peek_cooldown_moves = this.peekCooldownMoves;
8861
+ this.updateHintsDisplay();
8862
+ }
8863
+ }
8864
+
8865
+ getWrongFlipBackDelayMs() {
8866
+ const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
8867
+
8868
+ // Level 1-5: 900ms, Level 6-12: 700ms, Level 13+: 550ms.
8869
+ if (level <= 5) return 900;
8870
+ if (level <= 12) return 700;
8871
+ return 550;
8872
+ }
8873
+
8874
+ getCompletedChallengeLevels() {
8875
+ return Math.max(0, Number(this.state.completed_challenge_levels || 0));
8876
+ }
8877
+
8878
+ rememberRecentlyFlipped(index) {
8879
+ const value = Number(index);
8880
+ if (!Number.isFinite(value)) return;
8881
+ this.recentlyFlippedCardIndexes = [
8882
+ value,
8883
+ ...this.recentlyFlippedCardIndexes.filter((item) => item !== value),
8884
+ ].slice(0, 6);
8885
+ }
8886
+
8887
  getDistractorSparkleIntervalMs() {
8888
  const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
8889
 
 
8896
  return;
8897
  }
8898
 
8899
+ const faceDownCandidates = Array.from(this.grid.querySelectorAll('.memory-card:not(.matched):not(.flipped):not(.preview-locked)'));
8900
+ const recentlyFlippedCandidates = this.recentlyFlippedCardIndexes
8901
+ .map((index) => this.grid.querySelector(`[data-index="${index}"]`))
8902
+ .filter((card) => card && !card.classList.contains('matched') && !card.classList.contains('preview-locked'));
8903
+
8904
+ const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
8905
+ const canUseRecentDecoy = level >= 8 && recentlyFlippedCandidates.length > 0;
8906
+ const useRecentDecoy = canUseRecentDecoy && Math.random() < 0.34;
8907
+ const candidates = useRecentDecoy ? recentlyFlippedCandidates : faceDownCandidates;
8908
  if (!candidates.length) return;
8909
 
8910
  const card = candidates[Math.floor(Math.random() * candidates.length)];
 
8974
  this.hints <= 0 ||
8975
  this.state.status !== 'playing' ||
8976
  this.hinting ||
8977
+ this.peekCooldownMoves > 0 ||
8978
  oddFaceUp ||
8979
  this.state.game_over ||
8980
  this.state.level_complete;
 
9645
  });
9646
  }
9647
 
9648
+ clearPreviewChallengeEffects() {
9649
+ this.previewFogTimers.forEach((timer) => clearTimeout(timer));
9650
+ this.previewFogTimers = [];
9651
+ this.previewHideTimers.forEach((timer) => clearTimeout(timer));
9652
+ this.previewHideTimers = [];
9653
+
9654
+ const effectRoots = [this.memoryContainer, this.root].filter(Boolean);
9655
+ effectRoots.forEach((root) => root.classList.remove('preview-fog-blink'));
9656
+
9657
+ if (this.grid) {
9658
+ this.grid.querySelectorAll('.memory-card.preview-hidden-card').forEach((card) => {
9659
+ card.classList.remove('preview-hidden-card');
9660
+ });
9661
+ }
9662
+ }
9663
+
9664
+ triggerPreviewFogBlinkOnce() {
9665
+ const effectRoot = this.memoryContainer || this.root;
9666
+ if (!effectRoot || this.state.status !== 'preview') return;
9667
+
9668
+ effectRoot.classList.remove('preview-fog-blink');
9669
+ void effectRoot.offsetWidth;
9670
+ effectRoot.classList.add('preview-fog-blink');
9671
+
9672
+ const timer = setTimeout(() => {
9673
+ effectRoot.classList.remove('preview-fog-blink');
9674
+ }, 280);
9675
+ this.previewFogTimers.push(timer);
9676
+ }
9677
+
9678
+ startPreviewFogChallenge() {
9679
+ const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
9680
+ if (level < 10) return;
9681
+
9682
+ const blinkCount = level >= 18 ? 2 : 1;
9683
+ const delays = blinkCount >= 2 ? [760, 1760] : [1260];
9684
+ delays.slice(0, blinkCount).forEach((delay) => {
9685
+ const timer = setTimeout(() => {
9686
+ if (window.__activeMemoryLevelId === this.levelId && this.state.status === 'preview') {
9687
+ this.triggerPreviewFogBlinkOnce();
9688
+ }
9689
+ }, delay);
9690
+ this.previewFogTimers.push(timer);
9691
+ });
9692
+ }
9693
+
9694
+ getPreviewHideCardCount() {
9695
+ const completedChallengeLevels = this.getCompletedChallengeLevels();
9696
+ if (completedChallengeLevels < 1) return 0;
9697
+ return completedChallengeLevels >= 3 ? 2 : 1;
9698
+ }
9699
+
9700
+ hideRandomPreviewCardsOnce() {
9701
+ if (!this.grid || this.state.status !== 'preview') return;
9702
+
9703
+ const hideCount = this.getPreviewHideCardCount();
9704
+ if (hideCount <= 0) return;
9705
+
9706
+ const cards = Array.from(this.grid.querySelectorAll('.memory-card.preview-locked:not(.matched):not(.preview-hidden-card)'));
9707
+ if (!cards.length) return;
9708
+
9709
+ const shuffled = this.makeDifferentPreviewOrder(cards).slice(0, Math.min(hideCount, cards.length));
9710
+ shuffled.forEach((card) => {
9711
+ card.classList.add('preview-hidden-card');
9712
+ const timer = setTimeout(() => {
9713
+ card.classList.remove('preview-hidden-card');
9714
+ }, 650);
9715
+ this.previewHideTimers.push(timer);
9716
+ });
9717
+ }
9718
+
9719
+ startPreviewHideChallenge() {
9720
+ if (this.getPreviewHideCardCount() <= 0) return;
9721
+
9722
+ [920, 1880].forEach((delay, index) => {
9723
+ const timer = setTimeout(() => {
9724
+ if (window.__activeMemoryLevelId === this.levelId && this.state.status === 'preview') {
9725
+ // The second pulse only appears once challenge progress is stronger.
9726
+ if (index === 0 || this.getCompletedChallengeLevels() >= 2) {
9727
+ this.hideRandomPreviewCardsOnce();
9728
+ }
9729
+ }
9730
+ }, delay);
9731
+ this.previewHideTimers.push(timer);
9732
+ });
9733
+ }
9734
+
9735
+ startPreviewDifficultyEffects() {
9736
+ this.clearPreviewChallengeEffects();
9737
+ this.startPreviewFogChallenge();
9738
+ this.startPreviewHideChallenge();
9739
+ }
9740
+
9741
  startPreviewGridShuffle() {
9742
  this.stopPreviewGridShuffle();
9743
 
 
9764
  this.previewShuffleTimer = null;
9765
  }
9766
 
9767
+ this.clearPreviewChallengeEffects();
9768
+
9769
  if (!this.grid) return;
9770
 
9771
  this.grid.querySelectorAll('.memory-card').forEach((card) => {
 
9821
  });
9822
  cardEl.style.transform = '';
9823
  cardEl.style.zIndex = '';
9824
+ cardEl.classList.remove('preview-hidden-card', 'distractor-sparkle');
9825
  });
9826
  }
9827
 
 
9873
  this.state.status = 'preview';
9874
  this.state.level_complete = false;
9875
  this.state.game_over = false;
9876
+ this.recentlyFlippedCardIndexes = [];
9877
+ this.peekCooldownMoves = Number(this.state.peek_cooldown_moves || 0);
9878
  this.syncProgressiveHintLimit(true);
9879
  this.applyStateStyle();
9880
  this.setStatus(
 
9903
 
9904
  setTimeout(() => {
9905
  this.startPreviewGridShuffle();
9906
+ this.startPreviewDifficultyEffects();
9907
  }, 260);
9908
  this.updateHintsDisplay();
9909
 
 
9937
  this.state.status = 'preview';
9938
  this.state.level_complete = false;
9939
  this.state.game_over = false;
9940
+ this.recentlyFlippedCardIndexes = [];
9941
+ this.peekCooldownMoves = Number(this.state.peek_cooldown_moves || 0);
9942
  this.syncProgressiveHintLimit(true);
9943
 
9944
  if (this.overlay) {
 
9970
 
9971
  setTimeout(() => {
9972
  this.startPreviewGridShuffle();
9973
+ this.startPreviewDifficultyEffects();
9974
  }, 260);
9975
 
9976
  this.previewTimer = setInterval(() => {
 
10157
 
10158
  card.classList.add('flipped');
10159
  this.flipped.push(idx);
10160
+ this.rememberRecentlyFlipped(idx);
10161
 
10162
  if (this.flipped.length === 1) {
10163
  this.updateHintsDisplay();
 
10172
  this.locked = true;
10173
  this.moves += 1;
10174
  this.state.moves = this.moves;
10175
+ this.decrementPeekCooldownAfterMove();
10176
  this.updateHud();
10177
 
10178
  const [a, b] = this.flipped;
 
10292
  this.resetComboFeedback();
10293
  this.clearWrongMatchTimer();
10294
  this.locked = true;
10295
+ const wrongFlipBackDelay = this.getWrongFlipBackDelayMs();
10296
  this.wrongMatchTimer = window.setTimeout(() => {
10297
  try {
10298
  [a, b].forEach((index) => {
 
10309
  this.gameOver();
10310
  }
10311
  }
10312
+ }, wrongFlipBackDelay);
10313
 
10314
  try {
10315
  this.showFeedbackOverlay('WRONG MATCH', 'Lost 1 live', 'negative', false);
 
10343
  if (this.state.status !== 'playing') return;
10344
  this.syncProgressiveHintLimit(false);
10345
  if (this.hinting) return;
10346
+ if (this.peekCooldownMoves > 0) {
10347
+ this.setStatus('Peek is cooling down.', 'error', `${this.peekCooldownMoves} move${this.peekCooldownMoves === 1 ? '' : 's'} left before next peek.`);
10348
+ this.updateHintsDisplay();
10349
+ return;
10350
+ }
10351
  if (this.hints <= 0) {
10352
  this.setStatus('No peeks left!', 'error', '');
10353
  this.updateHintsDisplay();
 
10390
 
10391
  this.hinting = false;
10392
  this.locked = false;
10393
+ this.peekCooldownMoves = this.getHintCooldownLengthMoves();
10394
+ this.state.peek_cooldown_moves = this.peekCooldownMoves;
10395
  this.updateHintsDisplay();
10396
  await this.syncBackendEvent('hint');
10397
  }, 1200);