📂 L12
├── 📄 index.html
├── 📄 exp.js
├── 📄 style.css
├── 📂 images
├── 📂 audio
└── 📂 jspsych26 Lab 12: Implicit Learning
26.1 Research in Brief
26.1.1 The Research Area
Implicit learning refers to the acquisition of knowledge about patterns, regularities, or relationships in the environment without conscious awareness or intention to learn. Unlike explicit learning, where we deliberately study material with full awareness of what we are trying to master, implicit learning occurs as a byproduct of performing tasks and interacting with our environment. This type of learning allows us to pick up complex regularities and statistical patterns that would be difficult to learn through conscious effort alone.
Research on implicit learning addresses fundamental questions about the nature of consciousness, attention, and memory. Can we learn without being aware of what we are learning? Does attention play a role in acquiring implicit knowledge? How do implicitly learned patterns influence behavior? Understanding implicit learning has important implications for skill acquisition, language development, and the automatization of complex behaviors.
Implicit learning also connects to practical applications in areas such as motor skill development, language acquisition, and the design of training programs. Many everyday skills, from typing and driving to playing a musical instrument, involve components that are learned implicitly through repeated exposure rather than explicit instruction. To investigate these learning mechanisms systematically, researchers have developed experimental paradigms that can measure learning while controlling for conscious awareness and intentional strategies.
26.1.2 Types of Learning and Knowledge
Learning can be characterized along several dimensions. Explicit learning involves conscious effort and awareness of what is being learned, such as studying vocabulary words or memorizing mathematical formulas. Implicit learning occurs without conscious awareness of the learning process or the knowledge being acquired, such as learning the grammatical structure of your native language as a child.
The distinction between implicit and explicit learning relates closely to different types of knowledge. Declarative knowledge refers to facts and information that can be consciously recalled and verbally expressed. Procedural knowledge involves skills and procedures that guide behavior but may not be easily articulated, such as knowing how to ride a bicycle or type on a keyboard.
Research distinguishes between learning that requires attention and learning that can occur automatically. Some forms of implicit learning appear to require attentional resources, while others may proceed even when attention is divided across multiple tasks. The relationship between attention and implicit learning remains an active area of investigation, with implications for understanding the mechanisms underlying skill acquisition.
26.1.3 The Research Design: The Serial Reaction Time Task
The serial reaction time (SRT) task developed by Nissen and Bullemer (1987) uses a within-subjects experimental design to examine how people learn sequential patterns without explicit instruction or awareness. Participants respond to visual stimuli that follow either repeating sequences or random orders.
Stimulus Presentation: Participants view a display containing four possible target locations, typically arranged horizontally. On each trial, a target appears at one of these locations. The sequence of target locations either follows a repeating pattern (such as D-B-C-A-C-B-D-C-B-A, where letters represent the four positions from left to right) or appears in random order.
Task Requirements: Participants press corresponding keys as quickly and accurately as possible when targets appear. They are not informed about any repeating sequences and are simply instructed to respond to each target location. The task continues for multiple blocks of trials, allowing performance to be tracked over time.
Practice and Transfer Phases: The design typically includes practice blocks where participants respond to the repeating sequence, followed by transfer blocks where the sequence structure changes. In the forward sequence practice phase, participants respond to the standard repeating sequence. In the reversal testing phase, the sequence is reversed or replaced with random presentations to assess whether learning has occurred.
The within-subjects design allows researchers to compare response times during sequenced versus random blocks within the same participants. Each person experiences both conditions, acting as their own control. This approach isolates the specific effects of sequence learning while controlling for individual differences in overall response speed and motor ability. Performance improvements that are specific to the practiced sequence (and disrupted when the sequence changes) provide evidence of implicit learning.
26.1.4 Key Findings: Learning Without Awareness
Within-subjects comparisons have revealed that participants show substantial performance improvements when responding to repeating sequences compared to random presentations, even without awareness of the sequence structure. Response times typically decrease by 50-100 milliseconds for sequenced versus random trials after sufficient practice, with improvements developing gradually over several hundred trials.
When the sequence is changed to a random order or reversed during transfer blocks, response times increase significantly, returning to levels similar to early practice. This disruption demonstrates that learning was specific to the practiced sequence rather than reflecting general task improvement. The magnitude of this transfer cost provides a measure of how much sequence-specific knowledge was acquired.
Tests of explicit knowledge often reveal that many participants cannot verbally report the sequence or perform above chance when asked to predict upcoming locations. However, the relationship between explicit awareness and implicit learning remains debated. Some studies using process dissociation procedures have found evidence for implicit knowledge (sequence reproduction in conditions where participants try to avoid the sequence) that exists independently of explicit knowledge, particularly early in learning before explicit awareness develops.
26.1.5 Implications
The dissociation between performance improvements and conscious awareness provides evidence for implicit learning mechanisms that operate outside conscious control. This behavioral evidence is supported by neuroimaging studies showing that sequence learning involves motor areas and the basal ganglia associated with procedural learning, while explicit sequence knowledge involves hippocampal systems associated with declarative memory.
These findings support dual-process theories of learning and memory, demonstrating that knowledge can be acquired and expressed through multiple systems. The consistent patterns across participants suggest these represent fundamental properties of human learning rather than task-specific effects. Implicit learning may serve an adaptive function by allowing us to extract statistical regularities from the environment efficiently, freeing conscious resources for other demands.
The role of attention in implicit learning remains an important question. Evidence suggests that learning simple sequential associations may occur automatically without requiring attention, while learning more complex patterns with ambiguous relationships requires attentional resources. The response-effect relationship, where each response predicts the next stimulus location, appears to be a key component of what is learned in the SRT task. Some researchers propose that learning involves stimulus-response associations rather than pure perceptual or motor sequences.
26.1.6 Further Reading
Abrahamse, E. L., Jiménez, L., Verwey, W. B., & Clegg, B. A. (2010). Representing serial action and perception. Psychonomic Bulletin & Review, 17(5), 603-623.
Cleeremans, A., & McClelland, J. L. (1991). Learning the structure of event sequences. Journal of Experimental Psychology: General, 120(3), 235-253.
Nissen, M. J., & Bullemer, P. (1987). Attentional requirements of learning: Evidence from performance measures. Cognitive Psychology, 19(1), 1-32.
Schwarb, H., & Schumacher, E. H. (2012). Generalized lessons about sequence learning from the study of the serial reaction time task. Advances in Cognitive Psychology, 8(2), 165-178.
26.2 Gamify an Implicit Learning Task
In this lab tutorial, we will start with a typical laboratory version of the sequence learning task and add game elements to it.
26.2.1 A fully working experiment!
In this unit’s lab folder, you’ll find the code for a fully functional implicit motor learning task, as well as an additional folder containing image and audio files we’ll use to gamify this task.
The experiment follows a traditional sequence learning design, where participants press a key in response to the location of a target (here, a literal target image presented in one of four squares). The sequence typically repeats a number of times so that participants implicitly learn the pattern. At some point, the sequence is reversed. This test phase reveals how disruptive it is to perform the exact opposite of what they have been practicing.
In the programmed experiment, you’ll note that we have set only one repetition for both the learning and test phases. Typically, you would practice the sequence many times before switching to the test phase, and then switch back to the original sequence to see if performance improves again.
There is also a between-subjects condition assignment. One group of participants receives implicit instructions, where they are not made aware that the sequence is repeating. The other group receives explicit instructions, where they are told that the sequence repeats and are instructed to try to learn it.
Nothing in the code below should be surprising to you. Review the code to make sure you understand exactly what we’re starting with.
<!DOCTYPE html>
<html>
<head>
<title>Lab 12: Implicit Learning</title>
<!-- jsPsych -->
<script src="jspsych/jspsych.js"></script>
<link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
<!-- jPsych plugins -->
<script src="jspsych/plugin-instructions.js"></script>
<script src="jspsych/plugin-preload.js"></script>
<script src="jspsych/plugin-html-keyboard-response.js"></script>
<!-- custom CSS -->
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<!-- custom JS -->
<script src="exp.js"></script>
</body>
</html>
// ============================================
// Initiate jsPsych
// ============================================
const jsPsych = initJsPsych();
// ============================================
// Participant ID & Assign Condition
// ============================================
// Function to convert any string to a number
function stringToNumber(str) {
let sum = 0;
for (let i = 0; i < str.length; i++) {
sum += str.charCodeAt(i);
}
return sum;
}
// Get participant ID from URL, or generate random one if not found
let participant_id = jsPsych.data.getURLVariable("id") || jsPsych.randomization.randomInt(1, 100000).toString();
// Convert to number and assign condition
const numeric_id = stringToNumber(participant_id);
let conditions = ["implicit", "explicit"];
let assigned_condition = conditions[numeric_id % 2];
// Save id and assignment to data
jsPsych.data.addProperties({
participant_id: participant_id,
assigned_condition: assigned_condition
});
// ============================================
// Preload
// ============================================
let preload = {
type: jsPsychPreload,
images: ["images/black.png"]
}
// ============================================
// Welcome
// ============================================
let welcome = {
type: jsPsychHtmlKeyboardResponse,
stimulus: "Welcome to the experiment! Press the space bar to begin.",
choices: " "
}
// ============================================
// Instructions
// ============================================
let implicit_instructions = {
type: jsPsychInstructions,
pages: [`<div>
<p>In this experiment, you will see a target appear in one of four boxes. You will use the keyboard to respond:</p>
<ul>
<li>"D" if it is in the first box from the left</li>
<li>"F" if it is in the second box from the left</li>
<li>"J" if it is in the third box from the left</li>
<li>"K" if it is in the fourth box from the left</li>
</ul>
<p>When you are ready to begin, press any NEXT</p>
</div>`],
show_clickable_nav: true,
data: {
phase: "instructions"
}
}
let explicit_instructions = {
type: jsPsychInstructions,
pages: [`<div>
<p>In this experiment, you will see a target appear in one of four boxes.You will use the keyboard to respond:</p>
<ul>
<li>"D" if it is in the first box from the left</li>
<li>"F" if it is in the second box from the left</li>
<li>"J" if it is in the third box from the left</li>
<li>"K" if it is in the fourth box from the left</li>
</ul>
</div>`,
` <div>
<p>Important: The spatial location of the target will repeat in a predictable sequence. </p>
<p>Your task is to try to learn that sequence of locations while you complete the experiment </p>
<p>When you are ready to begin, press Next</p>
</div>`
],
show_clickable_nav: true,
data: {
phase: "instructions"
}
}
// ============================================
// Implicit Learning Task
// ============================================
let blank = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`,
trial_duration: 300,
choices: "NO_KEYS",
data: {
trial_part: "blank"
}
}
let target = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let output
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><img src="images/black.png" width="90px"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><img src="images/black.png" width="90px"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><img src="images/black.png" width="90px"></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><img src="images/black.png" width="90px"></div>
</div>`
}
return output
},
choices: ["d","f", "j", "k"],
data: {
trial_part: "target"
}
}
// learning phase in order
let learning = {
timeline: [
blank,
target
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
repetitions: 1,
data: {
phase: "learning",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
}
}
// test phase reverses the order
let test = {
timeline: [
blank,
target
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
sample: {
type: "custom",
fn: function(indices){
return indices.reverse(); // show the trials in the reverse order
}
},
repetitions: 1,
data: {
phase: "testing",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
}
}
// ============================================
// Savd Data Trial
// ============================================
const saveData = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `
<div style="text-align: center;">
<p>Experiment complete!</p>
<p>Click the button below to save your data locally:</p>
<button id="save-btn" style="padding: 10px 20px; font-size: 16px; cursor: pointer;">
Click here to save the data locally
</button>
</div>
`,
choices: "NO_KEYS",
trial_duration: null,
on_load: function() {
document.getElementById("save-btn").addEventListener("click", function() {
jsPsych.data.get().localSave("csv", "implicitLearning_data.csv");
});
},
data: {
phase: "save data trial"
}
};
// ============================================
// Run jsPsych
// ============================================
if(assigned_condition === "implicit"){
jsPsych.run([
preload,
welcome,
implicit_instructions,
learning,
test,
saveData
]);
} else if(assigned_condition === "explicit"){
jsPsych.run([
preload,
welcome,
explicit_instructions,
learning,
test,
saveData
]);
} 26.2.2 The End Result
There are three stages to this lab tutorial. Working through Stage 1 will produce a gamified version of the task that includes an instructional narrative, animated elements, and score indicators. Stage 2 adds audio elements, including feedback sound effects and background music. Stage 3 adds additional gameplay mechanics including a health indicator, a death/restart screen, and a personal high score indicator.
The end result will look like this:
<!DOCTYPE html>
<html>
<head>
<title>Lab 12: Implicit Learning</title>
<!-- jsPsych -->
<script src="jspsych/jspsych.js"></script>
<link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
<!-- jPsych plugins -->
<script src="jspsych/plugin-instructions.js"></script>
<script src="jspsych/plugin-preload.js"></script>
<script src="jspsych/plugin-html-keyboard-response.js"></script>
<!-- these are the google fonts -->
<link href="https://fonts.googleapis.com/css2?family=Creepster&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap"
rel="stylesheet">
<!-- custom CSS -->
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="game-display">
<div class="grid">
<div class="top-left">
<div class="score-display">
<div class="label">SCORE</div>
<div id="score" class="value">000000</div>
</div>
</div>
<div class="top">
<p class="game-title">WHACK-A-ZOMBIE!</p>
</div>
<div class="top-right">
<div class="score-display">
<div class="label">HIGH-SCORE</div>
<div id="high-score" class="value">000000</div>
</div>
</div>
<div class="left">
<div class="lives-display">
<div class="hearts">
<span>❤️</span>
<span>❤️</span>
<span>❤️</span>
<span>❤️</span>
<span>❤️</span>
</div>
</div>
</div>
<div id="jspsych-game-display" class="center"></div>
<div class="right"></div>
<div class="bottom-left"></div>
<div class="bottom"></div>
<div class="bottom-right"></div>
</div>
</div>
<!-- custom JS -->
<script src="exp.js"></script>
</body>
</html>
// ============================================
// Initiate jsPsych
// ============================================
const jsPsych = initJsPsych(
{display_element: 'jspsych-game-display'}
)
// ============================================
// Participant ID & Assign Condition
// ============================================
// Function to convert any string to a number
function stringToNumber(str) {
let sum = 0;
for (let i = 0; i < str.length; i++) {
sum += str.charCodeAt(i);
}
return sum;
}
// Get participant ID from URL, or generate random one if not found
let participant_id = jsPsych.data.getURLVariable("id") || jsPsych.randomization.randomInt(1, 100000).toString();
// Convert to number and assign condition
const numeric_id = stringToNumber(participant_id);
let conditions = ["implicit", "explicit"];
let assigned_condition = conditions[numeric_id % 2];
// Save id and assignment to data
jsPsych.data.addProperties({
participant_id: participant_id,
assigned_condition: assigned_condition
});
// ============================================
// Trackers
// ============================================
let score = 0
let hearts = 5
let high_score = 0
// ============================================
// Audio Setup
// ============================================
let sfx_coin = new Audio("audio/retro_coin.wav")
sfx_coin.volume = .3
let sfx_hit = new Audio("audio/retro_hit.wav")
sfx_hit.volume = .3
let sfx_squish = new Audio("audio/small_squish.mp3")
sfx_squish.volume = .1
let sfx_zombie = new Audio("audio/zombie-2.wav")
sfx_zombie.volume = .2
let bg_start = new Audio("audio/SeriousCutScene.wav")
bg_start.volume = .2
bg_start.loop = true
let bg_game = new Audio("audio/8BitMetal.wav")
bg_game.volume = .2
bg_game.loop = true
let bg_end = new Audio("audio/creep.mp3")
bg_end.volume = .2
bg_end.loop = true
// ============================================
// Preload
// ============================================
let preload = {
type: jsPsychPreload,
images: ["images/black.png", "images/bg.svg", "images/zombieAppear.png", "images/zombieIdle.png", "images/zombieDie.png", "images/zombieAttack.png"],
audio: ["audio/retro_coin.wav", "audio/retro_hit.wav", "audio/small_squish.mp3", "audio/zombie-2.wav",
"audio/SeriousCutScene.wav", "audio/8BitMetal.wav", "audio/creep.mp3"]
}
// ============================================
// Welcome
// ============================================
let welcome = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="instructionStyle">
<h3>PRESS ENTER TO START</h3>
</div>`,
choices: ["ENTER"]
}
// ============================================
// Instructions
// ============================================
let implicit_instructions = {
type: jsPsychInstructions,
pages: [`<div class="instructionStyle">
<p>Zombies are emerging from four graves in the cemetery!</p>
<p>Your mission: Whack them back down as quickly as possible using your keyboard:</p>
</div>`,
`<div class="instructionStyle">
<ul>
<li>Press "D" to whack the zombie in the first grave (leftmost)</li>
<li>Press "F" to whack the zombie in the second grave</li>
<li>Press "J" to whack the zombie in the third grave</li>
<li>Press "K" to whack the zombie in the fourth grave (rightmost)</li>
</ul>
</div>`,
`<div class="instructionStyle">
<p>Stay alert! Zombies will pop up randomly from different graves.</p>
<p>When you're ready to defend the cemetery, press NEXT</p>
</div>`],
show_clickable_nav: true,
on_start: function(){
bg_start.play();
},
on_finish: function(){
bg_start.pause();
bg_start.currentTime = 0;
}
}
let explicit_instructions = {
type: jsPsychInstructions,
pages: [`<div class="instructionStyle">
<p>Zombies are emerging from four graves in the cemetery!</p>
<p>Your mission: Whack them back down as quickly as possible using your keyboard:</p>
</div>`,
`<div class="instructionStyle">
<ul>
<li>Press "D" to whack the zombie in the first grave (leftmost)</li>
<li>Press "F" to whack the zombie in the second grave</li>
<li>Press "J" to whack the zombie in the third grave</li>
<li>Press "K" to whack the zombie in the fourth grave (rightmost)</li>
</ul>
</div>`,
`<div class="instructionStyle">
<p><strong>Secret Intel:</strong> These zombies follow a pattern!</p>
<p>The graves they emerge from repeat in a predictable sequence.</p>
<p>Your task is to learn this pattern while whacking zombies. The faster you learn it, the better you'll defend the cemetery!</p>
<p>When you're ready to begin your defense, press NEXT</p>
</div>`
],
show_clickable_nav: true,
on_start: function(){
bg_start.play();
},
on_finish: function(){
bg_start.pause();
bg_start.currentTime = 0;
}
}
// ============================================
// Implicit Learning Task
// ============================================
let blank = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`,
trial_duration: 300,
choices: "NO_KEYS",
data: {
trial_part: "blank"
}
}
let target = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let output
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
</div>`
}
return output
},
choices: ["d","f", "j", "k"],
data: {
trial_part: "target"
},
on_start: function(){
sfx_zombie.play();
}
}
let feedback = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let last_trial = jsPsych.data.get().last(1).values()
let last_response = last_trial[0].response
let last_accuracy = jsPsych.pluginAPI.compareKeys(last_response, jsPsych.evaluateTimelineVariable("correct_response"))
let feedback
if(last_accuracy){
feedback = "zombie-death"
score = score + 100
// play correct audio
sfx_coin.play();
sfx_squish.play();
} else {
feedback = "zombie-attack"
//score = score - 100
hearts = hearts - 1
let heart_display = document.querySelector(".hearts")
if(hearts == 5){
heart_display.innerHTML = `<span>❤️</span><span>❤️</span><span>❤️</span><span>❤️</span><span>❤️</span>`
} else if(hearts == 4){
heart_display.innerHTML = `<span>❤️</span><span>❤️</span><span>❤️</span><span>❤️</span><span>🖤</span>`
} else if(hearts == 3){
heart_display.innerHTML = `<span>❤️</span><span>❤️</span><span>❤️</span><span>🖤</span><span>🖤</span>`
} else if(hearts == 2){
heart_display.innerHTML = `<span>❤️</span><span>❤️</span><span>🖤</span><span>🖤</span><span>🖤</span>`
} else if(hearts == 1){
heart_display.innerHTML = `<span>❤️</span><span>🖤</span><span>🖤</span><span>🖤</span><span>🖤</span>`
} else if(hearts == 0){
heart_display.innerHTML = `<span>🖤</span><span>🖤</span><span>🖤</span><span>🖤</span><span>🖤</span>`
}
// play incorrect audio
sfx_hit.play();
}
if(score < 0){score = 0}
// update score
document.querySelector("#score").innerHTML = score.toString().padStart(6, "0");
// add animation to score
if(last_accuracy){
document.querySelector("#score").classList.add("heartbeat")
} else {
document.querySelector(".hearts").classList.add("shake-horizontal")
}
//update high score
if(score > high_score){
high_score = score
document.querySelector("#high-score").innerHTML = high_score.toString().padStart(6, "0");
document.querySelector("#high-score").classList.add("heartbeat")
}
// update output
let output
// still need the location
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
</div>`
}
return output
},
choices: "NO_KEYS",
trial_duration: 1200,
data: {
trial_part: "feedback"
},
on_finish: function(){
// make sure all audio has stopped and ready to start again
sfx_coin.pause();
sfx_hit.pause();
sfx_zombie.pause();
sfx_coin.currentTime = 0;
sfx_hit.currentTime = 0;
sfx_zombie.currentTime = 0;
// remove the animation classes
document.querySelector("#score").classList.remove("heartbeat")
document.querySelector(".hearts").classList.remove("shake-horizontal")
document.querySelector("#high-score").classList.remove("heartbeat")
}
}
// check death
let death = {
timeline: [
{
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="instructionStyle" style="text-align: center">
<h3 class="game-title" style="color:#ff00ff">You Died!</h3>
<p>Press "ENTER" to try again.</p>
</div>`,
choices: ["ENTER"],
trial_duration: null,
data: {trial_part: "death"},
on_start: function(){
bg_game.pause();
bg_end.play();
},
on_finish: function(){
bg_end.pause();
}
}
],
conditional_function: function(){
// this timeline runs IF hearts is 0 or less
if(hearts <= 0){
return true;
} else {
return false;
}
},
on_timeline_finish: function(){
// pause all music
bg_game.pause();
bg_game.currentTime = 0;
bg_start.pause();
bg_start.currentTime = 0;
bg_end.pause();
bg_end.currentTime = 0;
// end jsPsych early
jsPsych.abortExperiment();
// clear the jspsych display display
document.querySelector("#jspsych-game-display").innerHTML = ""
// restart the score/hearts
document.querySelector(".hearts").innerHTML = `<span>❤️</span><span>❤️</span><span>❤️</span><span>❤️</span><span>❤️</span>`
document.querySelector("#score").innerHTML = "000000"
score = 0
hearts = 5
// restart jspsych
if(assigned_condition === "implicit"){
jsPsych.run([
implicit_instructions,
learning,
test,
saveData
]);
} else if(assigned_condition === "explicit"){
jsPsych.run([
explicit_instructions,
learning,
test,
saveData
]);
}
}
}
// learning phase in order
let learning = {
timeline: [
blank,
target,
feedback,
death
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
repetitions: 1,
data: {
phase: "learning",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
},
on_timeline_start: function(){
bg_game.play();
}
}
// test phase reverses the order
let test = {
timeline: [
blank,
target,
feedback,
death
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
sample: {
type: "custom",
fn: function(indices){
return indices.reverse(); // show the trials in the reverse order
}
},
repetitions: 2,
data: {
phase: "testing",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
},
on_timeline_finish: function(){
bg_game.pause();
bg_game.currentTime = 0;
}
}
// ============================================
// Savd Data Trial
// ============================================
const saveData = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `
<div class="instructionStyle" style="text-align: center">
<p>GAME OVER!</p>
<p>Click the button below to save your data:</p>
<button class="jspsych-btn">
SAVE DATA
</button>
</div>
`,
choices: "NO_KEYS",
trial_duration: null,
on_load: function() {
document.getElementById("save-btn").addEventListener("click", function() {
jsPsych.data.get().localSave("csv", "implicitLearning_data.csv");
});
},
data: {
phase: "save data trial"
},
on_start: function(){
bg_end.play();
},
on_finish: function(){
bg_end.pause();
bg_end.currentTime = 0;
}
};
// ============================================
// Run jsPsych
// ============================================
if(assigned_condition === "implicit"){
jsPsych.run([
preload,
welcome,
implicit_instructions,
learning,
test,
saveData
]);
} else if(assigned_condition === "explicit"){
jsPsych.run([
preload,
welcome,
explicit_instructions,
learning,
test,
saveData
]);
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.boxContainer {
/* width: 500px;
height: 300px; */
display: flex;
align-items: center;
justify-content: center;
}
.box {
width: 120px;
height: 150px;
/*border: solid;*/
margin: 0px 10px 0px 10px;
padding: 0;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background-image: url("images/dirt.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: contain;
}
/* GAME DISPLAY */
.game-display {
margin: 0 auto;
width: 720px;
height: 420px;
background-image: url("images/bg.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: cover;
}
.grid {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: .5fr repeat(4, 1fr) .5fr;
grid-template-rows: repeat(8, 1fr);
grid-template-areas:
"top-left top-left top top top-right top-right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"bottom-left bottom bottom bottom bottom bottom-right";
}
/* Grid area assignments */
.top-left { grid-area: top-left; }
.top-right { grid-area: top-right; }
.top { grid-area: top; }
.center { grid-area: center; }
.left { grid-area: left; }
.right { grid-area: right; }
.bottom-left { grid-area: bottom-left; }
.bottom-right { grid-area: bottom-right; }
.bottom { grid-area: bottom; }
/* add a border, so we can see the divs */
/*.grid > div {
border: 1px solid white;
margin: -.5px;
}
*/
.game-title {
font-size: 36px;
text-align: center;
font-family: "Creepster", cursive;
color: darkred;
letter-spacing: .07em;
transform: rotate(-2deg);
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 20px rgba(255, 215, 0, 0.8);
}
/* zombie animation classes */
.zombie-appear {
width: 115px;
height: 150px;
animation:
appear .3s steps(11) forwards,
idle 1s steps(6) .3s infinite;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes appear {
from {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: -1265px 0;
}
}
@keyframes idle {
from {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: 0 0;
}
to {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: -584px 0;
}
}
.zombie-death {
width: 120px;
height: 150px;
animation: death .6s steps(8) forwards;
background-repeat: no-repeat;
background-position: 0 0;
background-size: auto 150px;
margin-bottom: 40px;
}
@keyframes death {
from {
background-image: url("images/zombieDie.png");
background-position: 0 0;
}
to {
background-image: url("images/zombieDie.png");
background-position: -960px 0;
}
}
.zombie-attack {
width: 120px;
height: 150px;
animation:
attack .6s steps(7) forwards,
appear .6s steps(11) .6s forwards reverse;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes attack {
from {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: -840px 0;
}
}
/* Score displays */
.score-display {
height: 100%;
width: 100%;
font-family: "Press Start 2P", system-ui;
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
text-align: center;
}
.score-display .label {
font-size: 15px;
color: #ffd700;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 8px rgba(255, 215, 0, 0.8);
}
.score-display .value {
font-size: 16px;
color: #00ff41;
text-shadow:
-2px -2px 0 #000,
2px -2px 0 #000,
-2px 2px 0 #000,
2px 2px 0 #000,
0 0 12px rgba(0, 255, 65, 0.9);
}
/* Lives display */
.lives-display {
font-family: "Press Start 2P", system-ui;
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
}
.hearts {
font-size: 16px;
filter: drop-shadow(0 0 5px #ff0066);
display: flex;
flex-direction: column;
gap: 5px;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 8px rgba(255, 215, 0, 0.8);
}
.instructionStyle {
text-align: left;
background-color: #0a0a0a;
border: 4px solid #00ff00;
box-shadow:
0 0 10px #00ff00,
inset 0 0 10px rgba(0, 255, 0, 0.2);
padding: 5px;
margin: 5px auto;
color: #00ff00;
font-family: "Press Start 2P", cursive;
line-height: 1.5;
text-shadow: 2px 2px 0px #003300;
}
.instructionStyle {
background-color: #1a0033;
border: 4px solid #ff00ff;
box-shadow: 0 0 15px #ff00ff;
color: #ff00ff;
text-shadow: 2px 2px 0px #330033;
}
.instructionStyle p {
margin: 5px 0;
font-size: 10px;
}
.instructionStyle ul {
list-style: none;
padding-left: 5px;
margin: 5px 0;
}
.instructionStyle li {
margin: 5px 0;
font-size: 10px;
}
.instructionStyle li::before {
content: "► ";
color: #ff0000;
margin-right: 10px;
}
/* Style the jsPsych navigation buttons */
.jspsych-btn {
background-color: #ff00ff;
color: #1a0033;
border: 3px solid #ff00ff;
padding: 5px 10px;
font-family: "Press Start 2P", cursive;
font-size: 12px;
cursor: pointer;
text-transform: uppercase;
box-shadow: 4px 4px 0px #660066;
transition: all 0.1s;
margin: 5px;
}
.jspsych-btn:hover {
background-color: #cc00cc;
transform: translate(2px, 2px);
box-shadow: 2px 2px 0px #660066;
}
.jspsych-btn:active {
transform: translate(4px, 4px);
box-shadow: 0px 0px 0px #660066;
}
.jspsych-btn:disabled {
background-color: #333333;
color: #666666;
border-color: #666666;
box-shadow: none;
cursor: not-allowed;
}
/* Style the button container */
.jspsych-instructions-nav {
margin-top: 30px;
text-align: center;
}
.heartbeat {
-webkit-animation: heartbeat 1.5s ease-in-out infinite both;
animation: heartbeat 1.5s ease-in-out infinite both;
}
/* ----------------------------------------------
* Generated by Animista on 2025-10-24 16:10:42
* Licensed under FreeBSD License.
* See http://animista.net/license for more info.
* w: http://animista.net, t: @cssanimista
* ---------------------------------------------- */
/**
* ----------------------------------------
* animation heartbeat
* ----------------------------------------
*/
@-webkit-keyframes heartbeat {
from {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-transform-origin: center center;
transform-origin: center center;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
10% {
-webkit-transform: scale(0.91);
transform: scale(0.91);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
17% {
-webkit-transform: scale(0.98);
transform: scale(0.98);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
33% {
-webkit-transform: scale(0.87);
transform: scale(0.87);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
45% {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
}
@keyframes heartbeat {
from {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-transform-origin: center center;
transform-origin: center center;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
10% {
-webkit-transform: scale(0.91);
transform: scale(0.91);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
17% {
-webkit-transform: scale(0.98);
transform: scale(0.98);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
33% {
-webkit-transform: scale(0.87);
transform: scale(0.87);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
45% {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
}
.shake-horizontal {
-webkit-animation: shake-horizontal 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) both;
animation: shake-horizontal 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) both;
}
/* ----------------------------------------------
* Generated by Animista on 2025-10-24 16:11:44
* Licensed under FreeBSD License.
* See http://animista.net/license for more info.
* w: http://animista.net, t: @cssanimista
* ---------------------------------------------- */
/**
* ----------------------------------------
* animation shake-horizontal
* ----------------------------------------
*/
@-webkit-keyframes shake-horizontal {
0%,
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
10%,
30%,
50%,
70% {
-webkit-transform: translateX(-10px);
transform: translateX(-10px);
}
20%,
40%,
60% {
-webkit-transform: translateX(10px);
transform: translateX(10px);
}
80% {
-webkit-transform: translateX(8px);
transform: translateX(8px);
}
90% {
-webkit-transform: translateX(-8px);
transform: translateX(-8px);
}
}
@keyframes shake-horizontal {
0%,
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
10%,
30%,
50%,
70% {
-webkit-transform: translateX(-10px);
transform: translateX(-10px);
}
20%,
40%,
60% {
-webkit-transform: translateX(10px);
transform: translateX(10px);
}
80% {
-webkit-transform: translateX(8px);
transform: translateX(8px);
}
90% {
-webkit-transform: translateX(-8px);
transform: translateX(-8px);
}
}
26.3 Stage 1: Basic Game with Animations
26.3.1 Create a Game Display
To begin, we’re going to create an HTML game display and insert the jsPsych experiment into the center grid area.
Changes:
index.html- Added the game display HTMLexp.js- updated the jsPsych init function and updated thepreloadtrialstyle.css- Added CSS to style the game display with a background image.
Additional Notes:
- We added a white border to visualize the divisions. We’ll remove this later
.boxContainer- removed the height/width settings (commented them out so they are ignored) to allow it fill the entire center grid spacebody- added styling so that our game display is centered on the screen
<!DOCTYPE html>
<html>
<head>
<title>Lab 12: Implicit Learning</title>
<!-- jsPsych -->
<script src="jspsych/jspsych.js"></script>
<link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
<!-- jPsych plugins -->
<script src="jspsych/plugin-instructions.js"></script>
<script src="jspsych/plugin-preload.js"></script>
<script src="jspsych/plugin-html-keyboard-response.js"></script>
<!-- custom CSS -->
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="game-display">
<div class="grid">
<div class="top-left">top-left</div>
<div class="top">top</div>
<div class="top-right">top-right</div>
<div class="left">left</div>
<div id="jspsych-game-display" class="center">center</div>
<div class="right">right</div>
<div class="bottom-left">bottom-left</div>
<div class="bottom">bottom</div>
<div class="bottom-right">bottom-right</div>
</div>
</div>
<!-- custom JS -->
<script src="exp.js"></script>
</body>
</html>
// ============================================
// Initiate jsPsych
// ============================================
const jsPsych = initJsPsych(
{display_element: 'jspsych-game-display'}
)
// ============================================
// Participant ID & Assign Condition
// ============================================
// Function to convert any string to a number
function stringToNumber(str) {
let sum = 0;
for (let i = 0; i < str.length; i++) {
sum += str.charCodeAt(i);
}
return sum;
}
// Get participant ID from URL, or generate random one if not found
let participant_id = jsPsych.data.getURLVariable("id") || jsPsych.randomization.randomInt(1, 100000).toString();
// Convert to number and assign condition
const numeric_id = stringToNumber(participant_id);
let conditions = ["implicit", "explicit"];
let assigned_condition = conditions[numeric_id % 2];
// Save id and assignment to data
jsPsych.data.addProperties({
participant_id: participant_id,
assigned_condition: assigned_condition
});
// ============================================
// Preload
// ============================================
let preload = {
type: jsPsychPreload,
images: ["images/black.png", "images/bg.svg"]
}
// ============================================
// Welcome
// ============================================
let welcome = {
type: jsPsychHtmlKeyboardResponse,
stimulus: "Welcome to the experiment! Press the space bar to begin.",
choices: " "
}
// ============================================
// Instructions
// ============================================
let implicit_instructions = {
type: jsPsychInstructions,
pages: [`<div>
<p>In this experiment, you will see a target appear in one of four boxes. You will use the keyboard to respond:</p>
<ul>
<li>"D" if it is in the first box from the left</li>
<li>"F" if it is in the second box from the left</li>
<li>"J" if it is in the third box from the left</li>
<li>"K" if it is in the fourth box from the left</li>
</ul>
<p>When you are ready to begin, press any NEXT</p>
</div>`],
show_clickable_nav: true,
data: {
phase: "instructions"
}
}
let explicit_instructions = {
type: jsPsychInstructions,
pages: [`<div>
<p>In this experiment, you will see a target appear in one of four boxes. You will use the keyboard to respond:</p>
<ul>
<li>"D" if it is in the first box from the left</li>
<li>"F" if it is in the second box from the left</li>
<li>"J" if it is in the third box from the left</li>
<li>"K" if it is in the fourth box from the left</li>
</ul>
</div>`,
`<div>
<p>Important: The spatial location of the target will repeat in a predictable sequence. </p>
<p>Your task is to try to learn that sequence of locations while you complete the experiment </p>
<p>When you are ready to begin, press Next</p>
</div>`
],
show_clickable_nav: true,
data: {
phase: "instructions"
}
}
// ============================================
// Implicit Learning Task
// ============================================
let blank = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`,
trial_duration: 300,
choices: "NO_KEYS",
data: {
trial_part: "blank"
}
}
let target = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let output
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><img src="images/black.png" width="90px"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><img src="images/black.png" width="90px"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><img src="images/black.png" width="90px"></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><img src="images/black.png" width="90px"></div>
</div>`
}
return output
},
choices: ["d","f", "j", "k"],
data: {
trial_part: "target"
}
}
// learning phase in order
let learning = {
timeline: [
blank,
target
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
repetitions: 1,
data: {
phase: "learning",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
}
}
// test phase reverses the order
let test = {
timeline: [
blank,
target
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
sample: {
type: "custom",
fn: function(indices){
return indices.reverse(); // show the trials in the reverse order
}
},
repetitions: 1,
data: {
phase: "testing",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
}
}
// ============================================
// Savd Data Trial
// ============================================
const saveData = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `
<div style="text-align: center;">
<p>Experiment complete!</p>
<p>Click the button below to save your data locally:</p>
<button id="save-btn" style="padding: 10px 20px; font-size: 16px; cursor: pointer;">
Click here to save the data locally
</button>
</div>
`,
choices: "NO_KEYS",
trial_duration: null,
on_load: function() {
document.getElementById("save-btn").addEventListener("click", function() {
jsPsych.data.get().localSave("csv", "implicitLearning_data.csv");
});
},
data: {
phase: "save data trial"
}
};
// ============================================
// Run jsPsych
// ============================================
if(assigned_condition === "implicit"){
jsPsych.run([
preload,
welcome,
implicit_instructions,
learning,
test,
saveData
]);
} else if(assigned_condition === "explicit"){
jsPsych.run([
preload,
welcome,
explicit_instructions,
learning,
test,
saveData
]);
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.boxContainer {
/* width: 500px;
height: 300px; */
display: flex;
align-items: center;
justify-content: center;
}
.box {
width: 115px;
height: 150px;
border: solid;
margin: 0px 10px 0px 10px;
padding: 0;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
/* GAME DISPLAY */
.game-display {
margin: 0 auto;
width: 720px;
height: 420px;
background-image: url("images/bg.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: cover;
}
.grid {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: .5fr repeat(4, 1fr) .5fr;
grid-template-rows: repeat(8, 1fr);
grid-template-areas:
"top-left top-left top top top-right top-right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"bottom-left bottom bottom bottom bottom bottom-right";
}
/* Grid area assignments */
.top-left { grid-area: top-left; }
.top-right { grid-area: top-right; }
.top { grid-area: top; }
.center { grid-area: center; }
.left { grid-area: left; }
.right { grid-area: right; }
.bottom-left { grid-area: bottom-left; }
.bottom-right { grid-area: bottom-right; }
.bottom { grid-area: bottom; }
/* add a border, so we can see the divs */
.grid > div {
border: 1px solid white;
margin: -.5px;
}
.game-title {
font-size: 24px;
text-align: center;
} 26.3.2 Replace Targets with Zombies
We’re going to replace the boxes with an image of a dirt pile and replace the target image with animated zombies.
Changes:
exp.js- Update the preload with zombie sprite sheets, replace the target image with zombie div in thetargettrialstyle.css- Update the.boxclass to remove the border and add the dirt and add the zombie animation classes
Additional Notes:
- Note that the sprite animation is actually two animations chained together. The first one is where the zombie pops out of the grave, the second one is the ‘idle’ movement, which is delayed so it happens after.
<!DOCTYPE html>
<html>
<head>
<title>Lab 12: Implicit Learning</title>
<!-- jsPsych -->
<script src="jspsych/jspsych.js"></script>
<link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
<!-- jPsych plugins -->
<script src="jspsych/plugin-instructions.js"></script>
<script src="jspsych/plugin-preload.js"></script>
<script src="jspsych/plugin-html-keyboard-response.js"></script>
<!-- custom CSS -->
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="game-display">
<div class="grid">
<div class="top-left">top-left</div>
<div class="top">top</div>
<div class="top-right">top-right</div>
<div class="left">left</div>
<div id="jspsych-game-display" class="center">center</div>
<div class="right">right</div>
<div class="bottom-left">bottom-left</div>
<div class="bottom">bottom</div>
<div class="bottom-right">bottom-right</div>
</div>
</div>
<!-- custom JS -->
<script src="exp.js"></script>
</body>
</html>
// ============================================
// Initiate jsPsych
// ============================================
const jsPsych = initJsPsych(
{display_element: 'jspsych-game-display'}
)
// ============================================
// Participant ID & Assign Condition
// ============================================
// Function to convert any string to a number
function stringToNumber(str) {
let sum = 0;
for (let i = 0; i < str.length; i++) {
sum += str.charCodeAt(i);
}
return sum;
}
// Get participant ID from URL, or generate random one if not found
let participant_id = jsPsych.data.getURLVariable("id") || jsPsych.randomization.randomInt(1, 100000).toString();
// Convert to number and assign condition
const numeric_id = stringToNumber(participant_id);
let conditions = ["implicit", "explicit"];
let assigned_condition = conditions[numeric_id % 2];
// Save id and assignment to data
jsPsych.data.addProperties({
participant_id: participant_id,
assigned_condition: assigned_condition
});
// ============================================
// Preload
// ============================================
let preload = {
type: jsPsychPreload,
images: ["images/black.png", "images/bg.svg", "images/zombieAppear.png", "images/zombieIdle.png", "images/zombieDie.png", "images/zombieAttack.png"]
}
// ============================================
// Welcome
// ============================================
let welcome = {
type: jsPsychHtmlKeyboardResponse,
stimulus: "Welcome to the experiment! Press the space bar to begin.",
choices: " "
}
// ============================================
// Instructions
// ============================================
let implicit_instructions = {
type: jsPsychInstructions,
pages: [`<div>
<p>In this experiment, you will see a target appear in one of four boxes. You will use the keyboard to respond:</p>
<ul>
<li>"D" if it is in the first box from the left</li>
<li>"F" if it is in the second box from the left</li>
<li>"J" if it is in the third box from the left</li>
<li>"K" if it is in the fourth box from the left</li>
</ul>
<p>When you are ready to begin, press any NEXT</p>
</div>`],
show_clickable_nav: true,
data: {
phase: "instructions"
}
}
let explicit_instructions = {
type: jsPsychInstructions,
pages: [`<div>
<p>In this experiment, you will see a target appear in one of four boxes. You will use the keyboard to respond:</p>
<ul>
<li>"D" if it is in the first box from the left</li>
<li>"F" if it is in the second box from the left</li>
<li>"J" if it is in the third box from the left</li>
<li>"K" if it is in the fourth box from the left</li>
</ul>
</div>`,
`<div>
<p>Important: The spatial location of the target will repeat in a predictable sequence. </p>
<p>Your task is to try to learn that sequence of locations while you complete the experiment </p>
<p>When you are ready to begin, press Next</p>
</div>`
],
show_clickable_nav: true,
data: {
phase: "instructions"
}
}
// ============================================
// Implicit Learning Task
// ============================================
let blank = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`,
trial_duration: 300,
choices: "NO_KEYS",
data: {
trial_part: "blank"
}
}
let target = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let output
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
</div>`
}
return output
},
choices: ["d","f", "j", "k"],
data: {
trial_part: "target"
}
}
// learning phase in order
let learning = {
timeline: [
blank,
target
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
repetitions: 1,
data: {
phase: "learning",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
}
}
// test phase reverses the order
let test = {
timeline: [
blank,
target
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
sample: {
type: "custom",
fn: function(indices){
return indices.reverse(); // show the trials in the reverse order
}
},
repetitions: 1,
data: {
phase: "testing",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
}
}
// ============================================
// Savd Data Trial
// ============================================
const saveData = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `
<div style="text-align: center;">
<p>Experiment complete!</p>
<p>Click the button below to save your data locally:</p>
<button id="save-btn" style="padding: 10px 20px; font-size: 16px; cursor: pointer;">
Click here to save the data locally
</button>
</div>
`,
choices: "NO_KEYS",
trial_duration: null,
on_load: function() {
document.getElementById("save-btn").addEventListener("click", function() {
jsPsych.data.get().localSave("csv", "implicitLearning_data.csv");
});
},
data: {
phase: "save data trial"
}
};
// ============================================
// Run jsPsych
// ============================================
if(assigned_condition === "implicit"){
jsPsych.run([
preload,
welcome,
implicit_instructions,
learning,
test,
saveData
]);
} else if(assigned_condition === "explicit"){
jsPsych.run([
preload,
welcome,
explicit_instructions,
learning,
test,
saveData
]);
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.boxContainer {
/* width: 500px;
height: 300px; */
display: flex;
align-items: center;
justify-content: center;
}
.box {
width: 115px;
height: 150px;
/*border: solid;*/
margin: 0px 10px 0px 10px;
padding: 0;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background-image: url("images/dirt.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: contain;
}
/* GAME DISPLAY */
.game-display {
margin: 0 auto;
width: 720px;
height: 420px;
background-image: url("images/bg.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: cover;
}
.grid {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: .5fr repeat(4, 1fr) .5fr;
grid-template-rows: repeat(8, 1fr);
grid-template-areas:
"top-left top-left top top top-right top-right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"bottom-left bottom bottom bottom bottom bottom-right";
}
/* Grid area assignments */
.top-left { grid-area: top-left; }
.top-right { grid-area: top-right; }
.top { grid-area: top; }
.center { grid-area: center; }
.left { grid-area: left; }
.right { grid-area: right; }
.bottom-left { grid-area: bottom-left; }
.bottom-right { grid-area: bottom-right; }
.bottom { grid-area: bottom; }
/* add a border, so we can see the divs */
.grid > div {
border: 1px solid white;
margin: -.5px;
}
.game-title {
font-size: 24px;
text-align: center;
}
/* zombie animation classes */
.zombie-appear {
width: 115px;
height: 150px;
animation:
appear .3s steps(11) forwards,
idle 1s steps(6) .3s infinite;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes appear {
from {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: -1265px 0;
}
}
@keyframes idle {
from {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: 0 0;
}
to {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: -584px 0;
}
}
26.3.3 Add Feedback Animations
We’ll add a feedback part to each trial, where either the zombie is squished after a correct response or the zombie attacks after an incorrect response.
Changes:
exp.js- Create the feedback trial and add it to both thelearningandtesttimelinesstyle.css- Add new zombie animations
Additional Notes:
- Note that the only difference between the correct/incorrect feedback display is the CSS animation class (
zombie-deathvs.zombie-attack). Instead of writing a series ofif elsestatements, I just swap the class name in thefeedbackvariable usingif elseand insert it into the HTML we’re already using.
<!DOCTYPE html>
<html>
<head>
<title>Lab 12: Implicit Learning</title>
<!-- jsPsych -->
<script src="jspsych/jspsych.js"></script>
<link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
<!-- jPsych plugins -->
<script src="jspsych/plugin-instructions.js"></script>
<script src="jspsych/plugin-preload.js"></script>
<script src="jspsych/plugin-html-keyboard-response.js"></script>
<!-- custom CSS -->
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="game-display">
<div class="grid">
<div class="top-left">top-left</div>
<div class="top">top</div>
<div class="top-right">top-right</div>
<div class="left">left</div>
<div id="jspsych-game-display" class="center">center</div>
<div class="right">right</div>
<div class="bottom-left">bottom-left</div>
<div class="bottom">bottom</div>
<div class="bottom-right">bottom-right</div>
</div>
</div>
<!-- custom JS -->
<script src="exp.js"></script>
</body>
</html>
// ============================================
// Initiate jsPsych
// ============================================
const jsPsych = initJsPsych(
{display_element: 'jspsych-game-display'}
)
// ============================================
// Participant ID & Assign Condition
// ============================================
// Function to convert any string to a number
function stringToNumber(str) {
let sum = 0;
for (let i = 0; i < str.length; i++) {
sum += str.charCodeAt(i);
}
return sum;
}
// Get participant ID from URL, or generate random one if not found
let participant_id = jsPsych.data.getURLVariable("id") || jsPsych.randomization.randomInt(1, 100000).toString();
// Convert to number and assign condition
const numeric_id = stringToNumber(participant_id);
let conditions = ["implicit", "explicit"];
let assigned_condition = conditions[numeric_id % 2];
// Save id and assignment to data
jsPsych.data.addProperties({
participant_id: participant_id,
assigned_condition: assigned_condition
});
// ============================================
// Preload
// ============================================
let preload = {
type: jsPsychPreload,
images: ["images/black.png", "images/bg.svg", "images/zombieAppear.png", "images/zombieIdle.png", "images/zombieDie.png", "images/zombieAttack.png"]
}
// ============================================
// Welcome
// ============================================
let welcome = {
type: jsPsychHtmlKeyboardResponse,
stimulus: "Welcome to the experiment! Press the space bar to begin.",
choices: " "
}
// ============================================
// Instructions
// ============================================
let implicit_instructions = {
type: jsPsychInstructions,
pages: [`<div>
<p>In this experiment, you will see a target appear in one of four boxes. You will use the keyboard to respond:</p>
<ul>
<li>"D" if it is in the first box from the left</li>
<li>"F" if it is in the second box from the left</li>
<li>"J" if it is in the third box from the left</li>
<li>"K" if it is in the fourth box from the left</li>
</ul>
<p>When you are ready to begin, press any NEXT</p>
</div>`],
show_clickable_nav: true,
data: {
phase: "instructions"
}
}
let explicit_instructions = {
type: jsPsychInstructions,
pages: [`<div>
<p>In this experiment, you will see a target appear in one of four boxes. You will use the keyboard to respond:</p>
<ul>
<li>"D" if it is in the first box from the left</li>
<li>"F" if it is in the second box from the left</li>
<li>"J" if it is in the third box from the left</li>
<li>"K" if it is in the fourth box from the left</li>
</ul>
</div>`,
`<div>
<p>Important: The spatial location of the target will repeat in a predictable sequence. </p>
<p>Your task is to try to learn that sequence of locations while you complete the experiment </p>
<p>When you are ready to begin, press Next</p>
</div>`
],
show_clickable_nav: true,
data: {
phase: "instructions"
}
}
// ============================================
// Implicit Learning Task
// ============================================
let blank = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`,
trial_duration: 300,
choices: "NO_KEYS",
data: {
trial_part: "blank"
}
}
let target = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let output
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
</div>`
}
return output
},
choices: ["d","f", "j", "k"],
data: {
trial_part: "target"
}
}
let feedback = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let last_trial = jsPsych.data.get().last(1).values()
let last_response = last_trial[0].response
let last_accuracy = jsPsych.pluginAPI.compareKeys(last_response, jsPsych.evaluateTimelineVariable("correct_response"))
let feedback
if(last_accuracy){
feedback = "zombie-death"
} else {
feedback = "zombie-attack"
}
// update output
let output
// still need the location
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
</div>`
}
return output
},
choices: "NO_KEYS",
trial_duration: 1200,
data: {
trial_part: "feedback"
}
}
// learning phase in order
let learning = {
timeline: [
blank,
target,
feedback
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
repetitions: 1,
data: {
phase: "learning",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
}
}
// test phase reverses the order
let test = {
timeline: [
blank,
target,
feedback
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
sample: {
type: "custom",
fn: function(indices){
return indices.reverse(); // show the trials in the reverse order
}
},
repetitions: 1,
data: {
phase: "testing",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
}
}
// ============================================
// Savd Data Trial
// ============================================
const saveData = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `
<div style="text-align: center;">
<p>Experiment complete!</p>
<p>Click the button below to save your data locally:</p>
<button id="save-btn" style="padding: 10px 20px; font-size: 16px; cursor: pointer;">
Click here to save the data locally
</button>
</div>
`,
choices: "NO_KEYS",
trial_duration: null,
on_load: function() {
document.getElementById("save-btn").addEventListener("click", function() {
jsPsych.data.get().localSave("csv", "implicitLearning_data.csv");
});
},
data: {
phase: "save data trial"
}
};
// ============================================
// Run jsPsych
// ============================================
if(assigned_condition === "implicit"){
jsPsych.run([
preload,
welcome,
implicit_instructions,
learning,
test,
saveData
]);
} else if(assigned_condition === "explicit"){
jsPsych.run([
preload,
welcome,
explicit_instructions,
learning,
test,
saveData
]);
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.boxContainer {
/* width: 500px;
height: 300px; */
display: flex;
align-items: center;
justify-content: center;
}
.box {
width: 120px;
height: 150px;
/*border: solid;*/
margin: 0px 10px 0px 10px;
padding: 0;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background-image: url("images/dirt.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: contain;
}
/* GAME DISPLAY */
.game-display {
margin: 0 auto;
width: 720px;
height: 420px;
background-image: url("images/bg.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: cover;
}
.grid {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: .5fr repeat(4, 1fr) .5fr;
grid-template-rows: repeat(8, 1fr);
grid-template-areas:
"top-left top-left top top top-right top-right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"bottom-left bottom bottom bottom bottom bottom-right";
}
/* Grid area assignments */
.top-left { grid-area: top-left; }
.top-right { grid-area: top-right; }
.top { grid-area: top; }
.center { grid-area: center; }
.left { grid-area: left; }
.right { grid-area: right; }
.bottom-left { grid-area: bottom-left; }
.bottom-right { grid-area: bottom-right; }
.bottom { grid-area: bottom; }
/* add a border, so we can see the divs */
.grid > div {
border: 1px solid white;
margin: -.5px;
}
.game-title {
font-size: 24px;
text-align: center;
}
/* zombie animation classes */
.zombie-appear {
width: 115px;
height: 150px;
animation:
appear .3s steps(11) forwards,
idle 1s steps(6) .3s infinite;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes appear {
from {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: -1265px 0;
}
}
@keyframes idle {
from {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: 0 0;
}
to {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: -584px 0;
}
}
.zombie-death {
width: 120px;
height: 150px;
animation: death .6s steps(8) forwards;
background-repeat: no-repeat;
background-position: 0 0;
background-size: auto 150px;
margin-bottom: 40px;
}
@keyframes death {
from {
background-image: url("images/zombieDie.png");
background-position: 0 0;
}
to {
background-image: url("images/zombieDie.png");
background-position: -960px 0;
}
}
.zombie-attack {
width: 120px;
height: 150px;
animation:
attack .6s steps(7) forwards,
appear .6s steps(11) .6s forwards reverse;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes attack {
from {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: -840px 0;
}
}
26.3.4 Update the Display
Changes:
index.html- Add links to the Google fonts, Add the score and high-score HTML to the displayexp.js- Add score tracker, Add logic to update the score after correct/incorrectstyle.css- Remove the white border, Update the styling for the title, Add styling to the score elements
Additional Notes:
- They get +100 for correct and -100 for incorrect. But I’ve added additional logic to prevent the score from ever going below 0. I used an additional built-in function
.toString().padStart(6, "0")to add leading 0s to the score. - Note the use of CSS selectors
.score-display .valuetargets any.valueclasses that are INSIDE of a.score-displaydiv. That way, I could re-use the.valueclass and give it different styling if it were inside a different kind of display.
<!DOCTYPE html>
<html>
<head>
<title>Lab 12: Implicit Learning</title>
<!-- jsPsych -->
<script src="jspsych/jspsych.js"></script>
<link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
<!-- jPsych plugins -->
<script src="jspsych/plugin-instructions.js"></script>
<script src="jspsych/plugin-preload.js"></script>
<script src="jspsych/plugin-html-keyboard-response.js"></script>
<!-- these are the google fonts -->
<link href="https://fonts.googleapis.com/css2?family=Creepster&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap"
rel="stylesheet">
<!-- custom CSS -->
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="game-display">
<div class="grid">
<div class="top-left">
<div class="score-display">
<div class="label">SCORE</div>
<div id="score" class="value">000000</div>
</div>
</div>
<div class="top">
<p class="game-title">WHACK-A-ZOMBIE!</p>
</div>
<div class="top-right">
<div class="score-display">
<div class="label">HIGH-SCORE</div>
<div id="high-score" class="value">999999</div>
</div>
</div>
<div class="left"></div>
<div id="jspsych-game-display" class="center"></div>
<div class="right"></div>
<div class="bottom-left"></div>
<div class="bottom"></div>
<div class="bottom-right"></div>
</div>
</div>
<!-- custom JS -->
<script src="exp.js"></script>
</body>
</html>
// ============================================
// Initiate jsPsych
// ============================================
const jsPsych = initJsPsych(
{display_element: 'jspsych-game-display'}
)
// ============================================
// Participant ID & Assign Condition
// ============================================
// Function to convert any string to a number
function stringToNumber(str) {
let sum = 0;
for (let i = 0; i < str.length; i++) {
sum += str.charCodeAt(i);
}
return sum;
}
// Get participant ID from URL, or generate random one if not found
let participant_id = jsPsych.data.getURLVariable("id") || jsPsych.randomization.randomInt(1, 100000).toString();
// Convert to number and assign condition
const numeric_id = stringToNumber(participant_id);
let conditions = ["implicit", "explicit"];
let assigned_condition = conditions[numeric_id % 2];
// Save id and assignment to data
jsPsych.data.addProperties({
participant_id: participant_id,
assigned_condition: assigned_condition
});
// ============================================
// Trackers
// ============================================
let score = 0
// ============================================
// Preload
// ============================================
let preload = {
type: jsPsychPreload,
images: ["images/black.png", "images/bg.svg", "images/zombieAppear.png", "images/zombieIdle.png", "images/zombieDie.png", "images/zombieAttack.png"]
}
// ============================================
// Welcome
// ============================================
let welcome = {
type: jsPsychHtmlKeyboardResponse,
stimulus: "Welcome to the experiment! Press the space bar to begin.",
choices: " "
}
// ============================================
// Instructions
// ============================================
let implicit_instructions = {
type: jsPsychInstructions,
pages: [`<div>
<p>In this experiment, you will see a target appear in one of four boxes. You will use the keyboard to respond:</p>
<ul>
<li>"D" if it is in the first box from the left</li>
<li>"F" if it is in the second box from the left</li>
<li>"J" if it is in the third box from the left</li>
<li>"K" if it is in the fourth box from the left</li>
</ul>
<p>When you are ready to begin, press any NEXT</p>
</div>`],
show_clickable_nav: true,
data: {
phase: "instructions"
}
}
let explicit_instructions = {
type: jsPsychInstructions,
pages: [`<div>
<p>In this experiment, you will see a target appear in one of four boxes. You will use the keyboard to respond:</p>
<ul>
<li>"D" if it is in the first box from the left</li>
<li>"F" if it is in the second box from the left</li>
<li>"J" if it is in the third box from the left</li>
<li>"K" if it is in the fourth box from the left</li>
</ul>
</div>`,
`<div>
<p>Important: The spatial location of the target will repeat in a predictable sequence. </p>
<p>Your task is to try to learn that sequence of locations while you complete the experiment </p>
<p>When you are ready to begin, press Next</p>
</div>`
],
show_clickable_nav: true,
data: {
phase: "instructions"
}
}
// ============================================
// Implicit Learning Task
// ============================================
let blank = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`,
trial_duration: 300,
choices: "NO_KEYS",
data: {
trial_part: "blank"
}
}
let target = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let output
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
</div>`
}
return output
},
choices: ["d","f", "j", "k"],
data: {
trial_part: "target"
}
}
let feedback = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let last_trial = jsPsych.data.get().last(1).values()
let last_response = last_trial[0].response
let last_accuracy = jsPsych.pluginAPI.compareKeys(last_response, jsPsych.evaluateTimelineVariable("correct_response"))
let feedback
if(last_accuracy){
feedback = "zombie-death"
score = score + 100
} else {
feedback = "zombie-attack"
score = score - 100
}
if(score < 0){score = 0}
// update score
document.querySelector("#score").innerHTML = score.toString().padStart(6, "0");
// update output
let output
// still need the location
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
</div>`
}
return output
},
choices: "NO_KEYS",
trial_duration: 1200,
data: {
trial_part: "feedback"
}
}
// learning phase in order
let learning = {
timeline: [
blank,
target,
feedback
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
repetitions: 1,
data: {
phase: "learning",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
}
}
// test phase reverses the order
let test = {
timeline: [
blank,
target,
feedback
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
sample: {
type: "custom",
fn: function(indices){
return indices.reverse(); // show the trials in the reverse order
}
},
repetitions: 1,
data: {
phase: "testing",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
}
}
// ============================================
// Savd Data Trial
// ============================================
const saveData = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `
<div style="text-align: center;">
<p>Experiment complete!</p>
<p>Click the button below to save your data locally:</p>
<button id="save-btn" style="padding: 10px 20px; font-size: 16px; cursor: pointer;">
Click here to save the data locally
</button>
</div>
`,
choices: "NO_KEYS",
trial_duration: null,
on_load: function() {
document.getElementById("save-btn").addEventListener("click", function() {
jsPsych.data.get().localSave("csv", "implicitLearning_data.csv");
});
},
data: {
phase: "save data trial"
}
};
// ============================================
// Run jsPsych
// ============================================
if(assigned_condition === "implicit"){
jsPsych.run([
preload,
welcome,
implicit_instructions,
learning,
test,
saveData
]);
} else if(assigned_condition === "explicit"){
jsPsych.run([
preload,
welcome,
explicit_instructions,
learning,
test,
saveData
]);
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.boxContainer {
/* width: 500px;
height: 300px; */
display: flex;
align-items: center;
justify-content: center;
}
.box {
width: 120px;
height: 150px;
/*border: solid;*/
margin: 0px 10px 0px 10px;
padding: 0;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background-image: url("images/dirt.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: contain;
}
/* GAME DISPLAY */
.game-display {
margin: 0 auto;
width: 720px;
height: 420px;
background-image: url("images/bg.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: cover;
}
.grid {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: .5fr repeat(4, 1fr) .5fr;
grid-template-rows: repeat(8, 1fr);
grid-template-areas:
"top-left top-left top top top-right top-right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"bottom-left bottom bottom bottom bottom bottom-right";
}
/* Grid area assignments */
.top-left { grid-area: top-left; }
.top-right { grid-area: top-right; }
.top { grid-area: top; }
.center { grid-area: center; }
.left { grid-area: left; }
.right { grid-area: right; }
.bottom-left { grid-area: bottom-left; }
.bottom-right { grid-area: bottom-right; }
.bottom { grid-area: bottom; }
/* add a border, so we can see the divs */
/*.grid > div {
border: 1px solid white;
margin: -.5px;
}
*/
.game-title {
font-size: 36px;
text-align: center;
font-family: "Creepster", cursive;
color: darkred;
letter-spacing: .07em;
transform: rotate(-2deg);
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 20px rgba(255, 215, 0, 0.8);
}
/* zombie animation classes */
.zombie-appear {
width: 115px;
height: 150px;
animation:
appear .3s steps(11) forwards,
idle 1s steps(6) .3s infinite;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes appear {
from {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: -1265px 0;
}
}
@keyframes idle {
from {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: 0 0;
}
to {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: -584px 0;
}
}
.zombie-death {
width: 120px;
height: 150px;
animation: death .6s steps(8) forwards;
background-repeat: no-repeat;
background-position: 0 0;
background-size: auto 150px;
margin-bottom: 40px;
}
@keyframes death {
from {
background-image: url("images/zombieDie.png");
background-position: 0 0;
}
to {
background-image: url("images/zombieDie.png");
background-position: -960px 0;
}
}
.zombie-attack {
width: 120px;
height: 150px;
animation:
attack .6s steps(7) forwards,
appear .6s steps(11) .6s forwards reverse;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes attack {
from {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: -840px 0;
}
}
/* Score displays */
.score-display {
height: 100%;
width: 100%;
font-family: "Press Start 2P", system-ui;
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
text-align: center;
}
.score-display .label {
font-size: 15px;
color: #ffd700;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 8px rgba(255, 215, 0, 0.8);
}
.score-display .value {
font-size: 16px;
color: #00ff41;
text-shadow:
-2px -2px 0 #000,
2px -2px 0 #000,
-2px 2px 0 #000,
2px 2px 0 #000,
0 0 12px rgba(0, 255, 65, 0.9);
}
26.3.5 Update Instructions, Welcome, & Game Over
We’ll update our instructions to give it a game-like narrative and re-style it so it looks like a retro game.
Changes:
exp.js- Update both the implicit and explicit instruction trials, Update the Welcome and Save Data trialsstyle.css- Create ainstructionStyleclass, re-write the defaultjspsych-btnclass
Additional Notes:
- I don’t claim to be a CSS expert! It took me quite a bit of trial-and-error to get the styling I was happy with (plus googling things like “how do I create a retro CSS text style”).
- Note the use of CSS selectors like
.instructionStyle p. This will re-style any<p>elements that are inside of ainstructionStyleelement. This is a helpful way of restyling all the paragraph tags without messing up the style of paragraph tags that are not part of the instructions.
<!DOCTYPE html>
<html>
<head>
<title>Lab 12: Implicit Learning</title>
<!-- jsPsych -->
<script src="jspsych/jspsych.js"></script>
<link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
<!-- jPsych plugins -->
<script src="jspsych/plugin-instructions.js"></script>
<script src="jspsych/plugin-preload.js"></script>
<script src="jspsych/plugin-html-keyboard-response.js"></script>
<!-- these are the google fonts -->
<link href="https://fonts.googleapis.com/css2?family=Creepster&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap"
rel="stylesheet">
<!-- custom CSS -->
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="game-display">
<div class="grid">
<div class="top-left">top-left</div>
<div class="top">top</div>
<div class="top-right">top-right</div>
<div class="left">left</div>
<div id="jspsych-game-display" class="center">center</div>
<div class="right">right</div>
<div class="bottom-left">bottom-left</div>
<div class="bottom">bottom</div>
<div class="bottom-right">bottom-right</div>
</div>
</div>
<!-- custom JS -->
<script src="exp.js"></script>
</body>
</html>
// ============================================
// Initiate jsPsych
// ============================================
const jsPsych = initJsPsych(
{display_element: 'jspsych-game-display'}
)
// ============================================
// Participant ID & Assign Condition
// ============================================
// Function to convert any string to a number
function stringToNumber(str) {
let sum = 0;
for (let i = 0; i < str.length; i++) {
sum += str.charCodeAt(i);
}
return sum;
}
// Get participant ID from URL, or generate random one if not found
let participant_id = jsPsych.data.getURLVariable("id") || jsPsych.randomization.randomInt(1, 100000).toString();
// Convert to number and assign condition
const numeric_id = stringToNumber(participant_id);
let conditions = ["implicit", "explicit"];
let assigned_condition = conditions[numeric_id % 2];
// Save id and assignment to data
jsPsych.data.addProperties({
participant_id: participant_id,
assigned_condition: assigned_condition
});
// ============================================
// Trackers
// ============================================
let score = 0
// ============================================
// Preload
// ============================================
let preload = {
type: jsPsychPreload,
images: ["images/black.png", "images/bg.svg", "images/zombieAppear.png", "images/zombieIdle.png", "images/zombieDie.png", "images/zombieAttack.png"]
}
// ============================================
// Welcome
// ============================================
let welcome = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="instructionStyle">
<h3>PRESS ENTER TO START</h3>
</div>`,
choices: ["ENTER"]
}
// ============================================
// Instructions
// ============================================
let implicit_instructions = {
type: jsPsychInstructions,
pages: [`<div class="instructionStyle">
<p>Zombies are emerging from four graves in the cemetery!</p>
<p>Your mission: Whack them back down as quickly as possible using your keyboard:</p>
</div>`,
`<div class="instructionStyle">
<ul>
<li>Press "D" to whack the zombie in the first grave (leftmost)</li>
<li>Press "F" to whack the zombie in the second grave</li>
<li>Press "J" to whack the zombie in the third grave</li>
<li>Press "K" to whack the zombie in the fourth grave (rightmost)</li>
</ul>
</div>`,
`<div class="instructionStyle">
<p>Stay alert! Zombies will pop up randomly from different graves.</p>
<p>When you're ready to defend the cemetery, press NEXT</p>
</div>`],
show_clickable_nav: true
}
let explicit_instructions = {
type: jsPsychInstructions,
pages: [`<div class="instructionStyle">
<p>Zombies are emerging from four graves in the cemetery!</p>
<p>Your mission: Whack them back down as quickly as possible using your keyboard:</p>
</div>`,
`<div class="instructionStyle">
<ul>
<li>Press "D" to whack the zombie in the first grave (leftmost)</li>
<li>Press "F" to whack the zombie in the second grave</li>
<li>Press "J" to whack the zombie in the third grave</li>
<li>Press "K" to whack the zombie in the fourth grave (rightmost)</li>
</ul>
</div>`,
`<div class="instructionStyle">
<p><strong>Secret Intel:</strong> These zombies follow a pattern!</p>
<p>The graves they emerge from repeat in a predictable sequence.</p>
<p>Your task is to learn this pattern while whacking zombies. The faster you learn it, the better you'll defend the cemetery!</p>
<p>When you're ready to begin your defense, press NEXT</p>
</div>`
],
show_clickable_nav: true
}
// ============================================
// Implicit Learning Task
// ============================================
let blank = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`,
trial_duration: 300,
choices: "NO_KEYS",
data: {
trial_part: "blank"
}
}
let target = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let output
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
</div>`
}
return output
},
choices: ["d","f", "j", "k"],
data: {
trial_part: "target"
}
}
let feedback = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let last_trial = jsPsych.data.get().last(1).values()
let last_response = last_trial[0].response
let last_accuracy = jsPsych.pluginAPI.compareKeys(last_response, jsPsych.evaluateTimelineVariable("correct_response"))
let feedback
if(last_accuracy){
feedback = "zombie-death"
score = score + 100
} else {
feedback = "zombie-attack"
score = score - 100
}
if(score < 0){score = 0}
// update score
document.querySelector("#score").innerHTML = score.toString().padStart(6, "0");
// update output
let output
// still need the location
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
</div>`
}
return output
},
choices: "NO_KEYS",
trial_duration: 1200,
data: {
trial_part: "feedback"
}
}
// learning phase in order
let learning = {
timeline: [
blank,
target,
feedback
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
repetitions: 1,
data: {
phase: "learning",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
}
}
// test phase reverses the order
let test = {
timeline: [
blank,
target,
feedback
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
sample: {
type: "custom",
fn: function(indices){
return indices.reverse(); // show the trials in the reverse order
}
},
repetitions: 1,
data: {
phase: "testing",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
}
}
// ============================================
// Savd Data Trial
// ============================================
const saveData = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="instructionStyle" style="text-align: center">
<p>GAME OVER!</p>
<p>Click the button below to save your data:</p>
<button class="jspsych-btn">
SAVE DATA
</button>
</div>`,
choices: "NO_KEYS",
trial_duration: null,
on_load: function() {
document.getElementById("save-btn").addEventListener("click", function() {
jsPsych.data.get().localSave("csv", "implicitLearning_data.csv");
});
},
data: {
phase: "save data trial"
}
};
// ============================================
// Run jsPsych
// ============================================
if(assigned_condition === "implicit"){
jsPsych.run([
preload,
welcome,
implicit_instructions,
learning,
test,
saveData
]);
} else if(assigned_condition === "explicit"){
jsPsych.run([
preload,
welcome,
explicit_instructions,
learning,
test,
saveData
]);
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.boxContainer {
/* width: 500px;
height: 300px; */
display: flex;
align-items: center;
justify-content: center;
}
.box {
width: 120px;
height: 150px;
/*border: solid;*/
margin: 0px 10px 0px 10px;
padding: 0;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background-image: url("images/dirt.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: contain;
}
/* GAME DISPLAY */
.game-display {
margin: 0 auto;
width: 720px;
height: 420px;
background-image: url("images/bg.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: cover;
}
.grid {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: .5fr repeat(4, 1fr) .5fr;
grid-template-rows: repeat(8, 1fr);
grid-template-areas:
"top-left top-left top top top-right top-right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"bottom-left bottom bottom bottom bottom bottom-right";
}
/* Grid area assignments */
.top-left { grid-area: top-left; }
.top-right { grid-area: top-right; }
.top { grid-area: top; }
.center { grid-area: center; }
.left { grid-area: left; }
.right { grid-area: right; }
.bottom-left { grid-area: bottom-left; }
.bottom-right { grid-area: bottom-right; }
.bottom { grid-area: bottom; }
/* add a border, so we can see the divs */
/*.grid > div {
border: 1px solid white;
margin: -.5px;
}
*/
.game-title {
font-size: 36px;
text-align: center;
font-family: "Creepster", cursive;
color: darkred;
letter-spacing: .07em;
transform: rotate(-2deg);
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 20px rgba(255, 215, 0, 0.8);
}
/* zombie animation classes */
.zombie-appear {
width: 115px;
height: 150px;
animation:
appear .3s steps(11) forwards,
idle 1s steps(6) .3s infinite;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes appear {
from {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: -1265px 0;
}
}
@keyframes idle {
from {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: 0 0;
}
to {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: -584px 0;
}
}
.zombie-death {
width: 120px;
height: 150px;
animation: death .6s steps(8) forwards;
background-repeat: no-repeat;
background-position: 0 0;
background-size: auto 150px;
margin-bottom: 40px;
}
@keyframes death {
from {
background-image: url("images/zombieDie.png");
background-position: 0 0;
}
to {
background-image: url("images/zombieDie.png");
background-position: -960px 0;
}
}
.zombie-attack {
width: 120px;
height: 150px;
animation:
attack .6s steps(7) forwards,
appear .6s steps(11) .6s forwards reverse;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes attack {
from {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: -840px 0;
}
}
/* Score displays */
.score-display {
height: 100%;
width: 100%;
font-family: "Press Start 2P", system-ui;
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
text-align: center;
}
.score-display .label {
font-size: 15px;
color: #ffd700;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 8px rgba(255, 215, 0, 0.8);
}
.score-display .value {
font-size: 16px;
color: #00ff41;
text-shadow:
-2px -2px 0 #000,
2px -2px 0 #000,
-2px 2px 0 #000,
2px 2px 0 #000,
0 0 12px rgba(0, 255, 65, 0.9);
}
/* Instruction Text Styling */
.instructionStyle {
text-align: left;
background-color: #1a0033;
border: 4px solid #ff00ff;
box-shadow: 0 0 15px #ff00ff;
padding: 5px;
margin: 5px auto;
color: #ff00ff;
font-family: "Press Start 2P", cursive;
line-height: 1.5;
text-shadow: 2px 2px 0px #330033;
}
.instructionStyle p {
margin: 5px 0;
font-size: 10px;
}
.instructionStyle ul {
list-style: none;
padding-left: 5px;
margin: 5px 0;
}
.instructionStyle li {
margin: 5px 0;
font-size: 10px;
}
.instructionStyle li::before {
content: "► ";
color: #ff0000;
margin-right: 10px;
}
/* Style the jsPsych navigation buttons */
.jspsych-btn {
background-color: #ff00ff;
color: #1a0033;
border: 3px solid #ff00ff;
padding: 5px 10px;
font-family: "Press Start 2P", cursive;
font-size: 12px;
cursor: pointer;
text-transform: uppercase;
box-shadow: 4px 4px 0px #660066;
transition: all 0.1s;
margin: 5px;
}
.jspsych-btn:hover {
background-color: #cc00cc;
transform: translate(2px, 2px);
box-shadow: 2px 2px 0px #660066;
}
.jspsych-btn:active {
transform: translate(4px, 4px);
box-shadow: 0px 0px 0px #660066;
}
.jspsych-btn:disabled {
background-color: #333333;
color: #666666;
border-color: #666666;
box-shadow: none;
cursor: not-allowed;
}
/* Style the button container */
.jspsych-instructions-nav {
margin-top: 30px;
text-align: center;
}
26.3.6 Additional Score Animations
As a final little flourish, let’s add some additional CSS animations to the score changes. I didn’t think it was rewarding/punishing enough yet, so I added a little ‘pulse’ to the score when you get points, and ‘shake’ when you lose points.
Changes:
exp.js- Use JavaScript to add the heartbeat or shake class to the score display, Updateon_finishto remove the class at the end of the trialstyle.css- Add the new animation classes
Additional Notes:
- Note that the animation is triggered when we add the class. But we can only add it if it doesn’t have the class already. Therefore, at the end of each trial, we need to make sure we remove the class, so we can add it again on the next feedback trial.
<!DOCTYPE html>
<html>
<head>
<title>Lab 12: Implicit Learning</title>
<!-- jsPsych -->
<script src="jspsych/jspsych.js"></script>
<link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
<!-- jPsych plugins -->
<script src="jspsych/plugin-instructions.js"></script>
<script src="jspsych/plugin-preload.js"></script>
<script src="jspsych/plugin-html-keyboard-response.js"></script>
<!-- these are the google fonts -->
<link href="https://fonts.googleapis.com/css2?family=Creepster&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap"
rel="stylesheet">
<!-- custom CSS -->
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="game-display">
<div class="grid">
<div class="top-left">top-left</div>
<div class="top">top</div>
<div class="top-right">top-right</div>
<div class="left">left</div>
<div id="jspsych-game-display" class="center">center</div>
<div class="right">right</div>
<div class="bottom-left">bottom-left</div>
<div class="bottom">bottom</div>
<div class="bottom-right">bottom-right</div>
</div>
</div>
<!-- custom JS -->
<script src="exp.js"></script>
</body>
</html>
// ============================================
// Initiate jsPsych
// ============================================
const jsPsych = initJsPsych(
{display_element: 'jspsych-game-display'}
)
// ============================================
// Participant ID & Assign Condition
// ============================================
// Function to convert any string to a number
function stringToNumber(str) {
let sum = 0;
for (let i = 0; i < str.length; i++) {
sum += str.charCodeAt(i);
}
return sum;
}
// Get participant ID from URL, or generate random one if not found
let participant_id = jsPsych.data.getURLVariable("id") || jsPsych.randomization.randomInt(1, 100000).toString();
// Convert to number and assign condition
const numeric_id = stringToNumber(participant_id);
let conditions = ["implicit", "explicit"];
let assigned_condition = conditions[numeric_id % 2];
// Save id and assignment to data
jsPsych.data.addProperties({
participant_id: participant_id,
assigned_condition: assigned_condition
});
// ============================================
// Trackers
// ============================================
let score = 0
// ============================================
// Preload
// ============================================
let preload = {
type: jsPsychPreload,
images: ["images/black.png", "images/bg.svg", "images/zombieAppear.png", "images/zombieIdle.png", "images/zombieDie.png", "images/zombieAttack.png"]
}
// ============================================
// Welcome
// ============================================
let welcome = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="instructionStyle">
<h3>PRESS ENTER TO START</h3>
</div>`,
choices: ["ENTER"]
}
// ============================================
// Instructions
// ============================================
let implicit_instructions = {
type: jsPsychInstructions,
pages: [`<div class="instructionStyle">
<p>Zombies are emerging from four graves in the cemetery!</p>
<p>Your mission: Whack them back down as quickly as possible using your keyboard:</p>
</div>`,
`<div class="instructionStyle">
<ul>
<li>Press "D" to whack the zombie in the first grave (leftmost)</li>
<li>Press "F" to whack the zombie in the second grave</li>
<li>Press "J" to whack the zombie in the third grave</li>
<li>Press "K" to whack the zombie in the fourth grave (rightmost)</li>
</ul>
</div>`,
`<div class="instructionStyle">
<p>Stay alert! Zombies will pop up randomly from different graves.</p>
<p>When you're ready to defend the cemetery, press NEXT</p>
</div>`],
show_clickable_nav: true
}
let explicit_instructions = {
type: jsPsychInstructions,
pages: [`<div class="instructionStyle">
<p>Zombies are emerging from four graves in the cemetery!</p>
<p>Your mission: Whack them back down as quickly as possible using your keyboard:</p>
</div>`,
`<div class="instructionStyle">
<ul>
<li>Press "D" to whack the zombie in the first grave (leftmost)</li>
<li>Press "F" to whack the zombie in the second grave</li>
<li>Press "J" to whack the zombie in the third grave</li>
<li>Press "K" to whack the zombie in the fourth grave (rightmost)</li>
</ul>
</div>`,
`<div class="instructionStyle">
<p><strong>Secret Intel:</strong> These zombies follow a pattern!</p>
<p>The graves they emerge from repeat in a predictable sequence.</p>
<p>Your task is to learn this pattern while whacking zombies. The faster you learn it, the better you'll defend the cemetery!</p>
<p>When you're ready to begin your defense, press NEXT</p>
</div>`
],
show_clickable_nav: true
}
// ============================================
// Implicit Learning Task
// ============================================
let blank = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`,
trial_duration: 300,
choices: "NO_KEYS",
data: {
trial_part: "blank"
}
}
let target = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let output
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
</div>`
}
return output
},
choices: ["d","f", "j", "k"],
data: {
trial_part: "target"
}
}
let feedback = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let last_trial = jsPsych.data.get().last(1).values()
let last_response = last_trial[0].response
let last_accuracy = jsPsych.pluginAPI.compareKeys(last_response, jsPsych.evaluateTimelineVariable("correct_response"))
let feedback
if(last_accuracy){
feedback = "zombie-death"
score = score + 100
} else {
feedback = "zombie-attack"
score = score - 100
}
if(score < 0){score = 0}
// update score
document.querySelector("#score").innerHTML = score.toString().padStart(6, "0");
// add animation to score
if(last_accuracy){
document.querySelector("#score").classList.add("heartbeat")
} else {
document.querySelector("#score").classList.add("shake-horizontal")
}
// update output
let output
// still need the location
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
</div>`
}
return output
},
choices: "NO_KEYS",
trial_duration: 1200,
data: {
trial_part: "feedback"
},
on_finish: function(){
// remove the animation classes
document.querySelector("#score").classList.remove("heartbeat")
document.querySelector("#score").classList.remove("shake-horizontal")
}
}
// learning phase in order
let learning = {
timeline: [
blank,
target,
feedback
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
repetitions: 1,
data: {
phase: "learning",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
}
}
// test phase reverses the order
let test = {
timeline: [
blank,
target,
feedback
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
sample: {
type: "custom",
fn: function(indices){
return indices.reverse(); // show the trials in the reverse order
}
},
repetitions: 1,
data: {
phase: "testing",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
}
}
// ============================================
// Savd Data Trial
// ============================================
const saveData = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `
<div class="instructionStyle" style="text-align: center">
<p>GAME OVER!</p>
<p>Click the button below to save your data:</p>
<button class="jspsych-btn">
SAVE DATA
</button>
</div>
`,
choices: "NO_KEYS",
trial_duration: null,
on_load: function() {
document.getElementById("save-btn").addEventListener("click", function() {
jsPsych.data.get().localSave("csv", "implicitLearning_data.csv");
});
},
data: {
phase: "save data trial"
}
};
// ============================================
// Run jsPsych
// ============================================
if(assigned_condition === "implicit"){
jsPsych.run([
preload,
welcome,
implicit_instructions,
learning,
test,
saveData
]);
} else if(assigned_condition === "explicit"){
jsPsych.run([
preload,
welcome,
explicit_instructions,
learning,
test,
saveData
]);
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.boxContainer {
/* width: 500px;
height: 300px; */
display: flex;
align-items: center;
justify-content: center;
}
.box {
width: 120px;
height: 150px;
/*border: solid;*/
margin: 0px 10px 0px 10px;
padding: 0;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background-image: url("images/dirt.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: contain;
}
/* GAME DISPLAY */
.game-display {
margin: 0 auto;
width: 720px;
height: 420px;
background-image: url("images/bg.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: cover;
}
.grid {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: .5fr repeat(4, 1fr) .5fr;
grid-template-rows: repeat(8, 1fr);
grid-template-areas:
"top-left top-left top top top-right top-right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"bottom-left bottom bottom bottom bottom bottom-right";
}
/* Grid area assignments */
.top-left { grid-area: top-left; }
.top-right { grid-area: top-right; }
.top { grid-area: top; }
.center { grid-area: center; }
.left { grid-area: left; }
.right { grid-area: right; }
.bottom-left { grid-area: bottom-left; }
.bottom-right { grid-area: bottom-right; }
.bottom { grid-area: bottom; }
/* add a border, so we can see the divs */
/*.grid > div {
border: 1px solid white;
margin: -.5px;
}
*/
.game-title {
font-size: 36px;
text-align: center;
font-family: "Creepster", cursive;
color: darkred;
letter-spacing: .07em;
transform: rotate(-2deg);
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 20px rgba(255, 215, 0, 0.8);
}
/* zombie animation classes */
.zombie-appear {
width: 115px;
height: 150px;
animation:
appear .3s steps(11) forwards,
idle 1s steps(6) .3s infinite;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes appear {
from {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: -1265px 0;
}
}
@keyframes idle {
from {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: 0 0;
}
to {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: -584px 0;
}
}
.zombie-death {
width: 120px;
height: 150px;
animation: death .6s steps(8) forwards;
background-repeat: no-repeat;
background-position: 0 0;
background-size: auto 150px;
margin-bottom: 40px;
}
@keyframes death {
from {
background-image: url("images/zombieDie.png");
background-position: 0 0;
}
to {
background-image: url("images/zombieDie.png");
background-position: -960px 0;
}
}
.zombie-attack {
width: 120px;
height: 150px;
animation:
attack .6s steps(7) forwards,
appear .6s steps(11) .6s forwards reverse;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes attack {
from {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: -840px 0;
}
}
/* Score displays */
.score-display {
height: 100%;
width: 100%;
font-family: "Press Start 2P", system-ui;
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
text-align: center;
}
.score-display .label {
font-size: 15px;
color: #ffd700;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 8px rgba(255, 215, 0, 0.8);
}
.score-display .value {
font-size: 16px;
color: #00ff41;
text-shadow:
-2px -2px 0 #000,
2px -2px 0 #000,
-2px 2px 0 #000,
2px 2px 0 #000,
0 0 12px rgba(0, 255, 65, 0.9);
}
.instructionStyle {
text-align: left;
background-color: #0a0a0a;
border: 4px solid #00ff00;
box-shadow:
0 0 10px #00ff00,
inset 0 0 10px rgba(0, 255, 0, 0.2);
padding: 5px;
margin: 5px auto;
color: #00ff00;
font-family: "Press Start 2P", cursive;
line-height: 1.5;
text-shadow: 2px 2px 0px #003300;
}
.instructionStyle {
background-color: #1a0033;
border: 4px solid #ff00ff;
box-shadow: 0 0 15px #ff00ff;
color: #ff00ff;
text-shadow: 2px 2px 0px #330033;
}
.instructionStyle p {
margin: 5px 0;
font-size: 10px;
}
.instructionStyle ul {
list-style: none;
padding-left: 5px;
margin: 5px 0;
}
.instructionStyle li {
margin: 5px 0;
font-size: 10px;
}
.instructionStyle li::before {
content: "► ";
color: #ff0000;
margin-right: 10px;
}
/* Style the jsPsych navigation buttons */
.jspsych-btn {
background-color: #ff00ff;
color: #1a0033;
border: 3px solid #ff00ff;
padding: 5px 10px;
font-family: "Press Start 2P", cursive;
font-size: 12px;
cursor: pointer;
text-transform: uppercase;
box-shadow: 4px 4px 0px #660066;
transition: all 0.1s;
margin: 5px;
}
.jspsych-btn:hover {
background-color: #cc00cc;
transform: translate(2px, 2px);
box-shadow: 2px 2px 0px #660066;
}
.jspsych-btn:active {
transform: translate(4px, 4px);
box-shadow: 0px 0px 0px #660066;
}
.jspsych-btn:disabled {
background-color: #333333;
color: #666666;
border-color: #666666;
box-shadow: none;
cursor: not-allowed;
}
/* Style the button container */
.jspsych-instructions-nav {
margin-top: 30px;
text-align: center;
}
.heartbeat {
-webkit-animation: heartbeat 1.5s ease-in-out infinite both;
animation: heartbeat 1.5s ease-in-out infinite both;
}
/* ----------------------------------------------
* Generated by Animista on 2025-10-24 16:10:42
* Licensed under FreeBSD License.
* See http://animista.net/license for more info.
* w: http://animista.net, t: @cssanimista
* ---------------------------------------------- */
/**
* ----------------------------------------
* animation heartbeat
* ----------------------------------------
*/
@-webkit-keyframes heartbeat {
from {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-transform-origin: center center;
transform-origin: center center;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
10% {
-webkit-transform: scale(0.91);
transform: scale(0.91);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
17% {
-webkit-transform: scale(0.98);
transform: scale(0.98);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
33% {
-webkit-transform: scale(0.87);
transform: scale(0.87);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
45% {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
}
@keyframes heartbeat {
from {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-transform-origin: center center;
transform-origin: center center;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
10% {
-webkit-transform: scale(0.91);
transform: scale(0.91);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
17% {
-webkit-transform: scale(0.98);
transform: scale(0.98);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
33% {
-webkit-transform: scale(0.87);
transform: scale(0.87);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
45% {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
}
.shake-horizontal {
-webkit-animation: shake-horizontal 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) both;
animation: shake-horizontal 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) both;
}
/* ----------------------------------------------
* Generated by Animista on 2025-10-24 16:11:44
* Licensed under FreeBSD License.
* See http://animista.net/license for more info.
* w: http://animista.net, t: @cssanimista
* ---------------------------------------------- */
/**
* ----------------------------------------
* animation shake-horizontal
* ----------------------------------------
*/
@-webkit-keyframes shake-horizontal {
0%,
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
10%,
30%,
50%,
70% {
-webkit-transform: translateX(-10px);
transform: translateX(-10px);
}
20%,
40%,
60% {
-webkit-transform: translateX(10px);
transform: translateX(10px);
}
80% {
-webkit-transform: translateX(8px);
transform: translateX(8px);
}
90% {
-webkit-transform: translateX(-8px);
transform: translateX(-8px);
}
}
@keyframes shake-horizontal {
0%,
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
10%,
30%,
50%,
70% {
-webkit-transform: translateX(-10px);
transform: translateX(-10px);
}
20%,
40%,
60% {
-webkit-transform: translateX(10px);
transform: translateX(10px);
}
80% {
-webkit-transform: translateX(8px);
transform: translateX(8px);
}
90% {
-webkit-transform: translateX(-8px);
transform: translateX(-8px);
}
}
26.4 Stage 2: Sound & Music
26.4.1 Add Sound Effects
Changes:
exp.js- Update the preload trial, add audio setup, Add audio play triggers, add audio clean up in on_finish function
Additional Notes:
- The audio files are short and should complete by the time the feedback trial finishes. But, it’s good practice to include fallbacks in case something fails. In this case, in the
on_finishfunction I ‘clean up’ the audio by pausing all the audio files and resetting thecurrentTimeto 0. This is probably unnecessary, but if I ever changed the trial feedback duration, this will prevent audio files from playing beyond the trial itself.
<!DOCTYPE html>
<html>
<head>
<title>Lab 12: Implicit Learning</title>
<!-- jsPsych -->
<script src="jspsych/jspsych.js"></script>
<link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
<!-- jPsych plugins -->
<script src="jspsych/plugin-instructions.js"></script>
<script src="jspsych/plugin-preload.js"></script>
<script src="jspsych/plugin-html-keyboard-response.js"></script>
<!-- these are the google fonts -->
<link href="https://fonts.googleapis.com/css2?family=Creepster&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap"
rel="stylesheet">
<!-- custom CSS -->
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="game-display">
<div class="grid">
<div class="top-left">top-left</div>
<div class="top">top</div>
<div class="top-right">top-right</div>
<div class="left">left</div>
<div id="jspsych-game-display" class="center">center</div>
<div class="right">right</div>
<div class="bottom-left">bottom-left</div>
<div class="bottom">bottom</div>
<div class="bottom-right">bottom-right</div>
</div>
</div>
<!-- custom JS -->
<script src="exp.js"></script>
</body>
</html>
// ============================================
// Initiate jsPsych
// ============================================
const jsPsych = initJsPsych(
{display_element: 'jspsych-game-display'}
)
// ============================================
// Participant ID & Assign Condition
// ============================================
// Function to convert any string to a number
function stringToNumber(str) {
let sum = 0;
for (let i = 0; i < str.length; i++) {
sum += str.charCodeAt(i);
}
return sum;
}
// Get participant ID from URL, or generate random one if not found
let participant_id = jsPsych.data.getURLVariable("id") || jsPsych.randomization.randomInt(1, 100000).toString();
// Convert to number and assign condition
const numeric_id = stringToNumber(participant_id);
let conditions = ["implicit", "explicit"];
let assigned_condition = conditions[numeric_id % 2];
// Save id and assignment to data
jsPsych.data.addProperties({
participant_id: participant_id,
assigned_condition: assigned_condition
});
// ============================================
// Trackers
// ============================================
let score = 0
// ============================================
// Audio Setup
// ============================================
let sfx_coin = new Audio("audio/retro_coin.wav")
sfx_coin.volume = .3
let sfx_hit = new Audio("audio/retro_hit.wav")
sfx_hit.volume = .3
let sfx_squish = new Audio("audio/small_squish.mp3")
sfx_squish.volume = .1
let sfx_zombie = new Audio("audio/zombie-2.wav")
sfx_zombie.volume = .2
// ============================================
// Preload
// ============================================
let preload = {
type: jsPsychPreload,
images: ["images/black.png", "images/bg.svg", "images/zombieAppear.png", "images/zombieIdle.png", "images/zombieDie.png", "images/zombieAttack.png"],
audio: ["audio/retro_coin.wav", "audio/retro_hit.wav", "audio/small_squish.mp3", "audio/zombie-2.wav"]
}
// ============================================
// Welcome
// ============================================
let welcome = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="instructionStyle">
<h3>PRESS ENTER TO START</h3>
</div>`,
choices: ["ENTER"]
}
// ============================================
// Instructions
// ============================================
let implicit_instructions = {
type: jsPsychInstructions,
pages: [`<div class="instructionStyle">
<p>Zombies are emerging from four graves in the cemetery!</p>
<p>Your mission: Whack them back down as quickly as possible using your keyboard:</p>
</div>`,
`<div class="instructionStyle">
<ul>
<li>Press "D" to whack the zombie in the first grave (leftmost)</li>
<li>Press "F" to whack the zombie in the second grave</li>
<li>Press "J" to whack the zombie in the third grave</li>
<li>Press "K" to whack the zombie in the fourth grave (rightmost)</li>
</ul>
</div>`,
`<div class="instructionStyle">
<p>Stay alert! Zombies will pop up randomly from different graves.</p>
<p>When you're ready to defend the cemetery, press NEXT</p>
</div>`],
show_clickable_nav: true
}
let explicit_instructions = {
type: jsPsychInstructions,
pages: [`<div class="instructionStyle">
<p>Zombies are emerging from four graves in the cemetery!</p>
<p>Your mission: Whack them back down as quickly as possible using your keyboard:</p>
</div>`,
`<div class="instructionStyle">
<ul>
<li>Press "D" to whack the zombie in the first grave (leftmost)</li>
<li>Press "F" to whack the zombie in the second grave</li>
<li>Press "J" to whack the zombie in the third grave</li>
<li>Press "K" to whack the zombie in the fourth grave (rightmost)</li>
</ul>
</div>`,
`<div class="instructionStyle">
<p><strong>Secret Intel:</strong> These zombies follow a pattern!</p>
<p>The graves they emerge from repeat in a predictable sequence.</p>
<p>Your task is to learn this pattern while whacking zombies. The faster you learn it, the better you'll defend the cemetery!</p>
<p>When you're ready to begin your defense, press NEXT</p>
</div>`
],
show_clickable_nav: true
}
// ============================================
// Implicit Learning Task
// ============================================
let blank = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`,
trial_duration: 300,
choices: "NO_KEYS",
data: {
trial_part: "blank"
}
}
let target = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let output
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
</div>`
}
return output
},
choices: ["d","f", "j", "k"],
data: {
trial_part: "target"
},
on_start: function(){
sfx_zombie.play();
}
}
let feedback = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let last_trial = jsPsych.data.get().last(1).values()
let last_response = last_trial[0].response
let last_accuracy = jsPsych.pluginAPI.compareKeys(last_response, jsPsych.evaluateTimelineVariable("correct_response"))
let feedback
if(last_accuracy){
feedback = "zombie-death"
score = score + 100
// play correct audio
sfx_coin.play();
sfx_squish.play();
} else {
feedback = "zombie-attack"
score = score - 100
// play incorrect audio
sfx_hit.play();
}
if(score < 0){score = 0}
// update score
document.querySelector("#score").innerHTML = score.toString().padStart(6, "0");
// add animation to score
if(last_accuracy){
document.querySelector("#score").classList.add("heartbeat")
} else {
document.querySelector("#score").classList.add("shake-horizontal")
}
// update output
let output
// still need the location
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
</div>`
}
return output
},
choices: "NO_KEYS",
trial_duration: 1200,
data: {
trial_part: "feedback"
},
on_finish: function(){
// make sure all audio has stopped and ready to start again
sfx_coin.pause();
sfx_hit.pause();
sfx_zombie.pause();
sfx_coin.currentTime = 0;
sfx_hit.currentTime = 0;
sfx_zombie.currentTime = 0;
// remove the animation classes
document.querySelector("#score").classList.remove("heartbeat")
document.querySelector("#score").classList.remove("shake-horizontal")
}
}
// learning phase in order
let learning = {
timeline: [
blank,
target,
feedback
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
repetitions: 1,
data: {
phase: "learning",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
}
}
// test phase reverses the order
let test = {
timeline: [
blank,
target,
feedback
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
sample: {
type: "custom",
fn: function(indices){
return indices.reverse(); // show the trials in the reverse order
}
},
repetitions: 1,
data: {
phase: "testing",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
}
}
// ============================================
// Savd Data Trial
// ============================================
const saveData = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `
<div class="instructionStyle" style="text-align: center">
<p>GAME OVER!</p>
<p>Click the button below to save your data:</p>
<button class="jspsych-btn">
SAVE DATA
</button>
</div>
`,
choices: "NO_KEYS",
trial_duration: null,
on_load: function() {
document.getElementById("save-btn").addEventListener("click", function() {
jsPsych.data.get().localSave("csv", "implicitLearning_data.csv");
});
},
data: {
phase: "save data trial"
}
};
// ============================================
// Run jsPsych
// ============================================
if(assigned_condition === "implicit"){
jsPsych.run([
preload,
welcome,
implicit_instructions,
learning,
test,
saveData
]);
} else if(assigned_condition === "explicit"){
jsPsych.run([
preload,
welcome,
explicit_instructions,
learning,
test,
saveData
]);
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.boxContainer {
/* width: 500px;
height: 300px; */
display: flex;
align-items: center;
justify-content: center;
}
.box {
width: 120px;
height: 150px;
/*border: solid;*/
margin: 0px 10px 0px 10px;
padding: 0;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background-image: url("images/dirt.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: contain;
}
/* GAME DISPLAY */
.game-display {
margin: 0 auto;
width: 720px;
height: 420px;
background-image: url("images/bg.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: cover;
}
.grid {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: .5fr repeat(4, 1fr) .5fr;
grid-template-rows: repeat(8, 1fr);
grid-template-areas:
"top-left top-left top top top-right top-right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"bottom-left bottom bottom bottom bottom bottom-right";
}
/* Grid area assignments */
.top-left { grid-area: top-left; }
.top-right { grid-area: top-right; }
.top { grid-area: top; }
.center { grid-area: center; }
.left { grid-area: left; }
.right { grid-area: right; }
.bottom-left { grid-area: bottom-left; }
.bottom-right { grid-area: bottom-right; }
.bottom { grid-area: bottom; }
/* add a border, so we can see the divs */
/*.grid > div {
border: 1px solid white;
margin: -.5px;
}
*/
.game-title {
font-size: 36px;
text-align: center;
font-family: "Creepster", cursive;
color: darkred;
letter-spacing: .07em;
transform: rotate(-2deg);
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 20px rgba(255, 215, 0, 0.8);
}
/* zombie animation classes */
.zombie-appear {
width: 115px;
height: 150px;
animation:
appear .3s steps(11) forwards,
idle 1s steps(6) .3s infinite;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes appear {
from {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: -1265px 0;
}
}
@keyframes idle {
from {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: 0 0;
}
to {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: -584px 0;
}
}
.zombie-death {
width: 120px;
height: 150px;
animation: death .6s steps(8) forwards;
background-repeat: no-repeat;
background-position: 0 0;
background-size: auto 150px;
margin-bottom: 40px;
}
@keyframes death {
from {
background-image: url("images/zombieDie.png");
background-position: 0 0;
}
to {
background-image: url("images/zombieDie.png");
background-position: -960px 0;
}
}
.zombie-attack {
width: 120px;
height: 150px;
animation:
attack .6s steps(7) forwards,
appear .6s steps(11) .6s forwards reverse;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes attack {
from {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: -840px 0;
}
}
/* Score displays */
.score-display {
height: 100%;
width: 100%;
font-family: "Press Start 2P", system-ui;
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
text-align: center;
}
.score-display .label {
font-size: 15px;
color: #ffd700;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 8px rgba(255, 215, 0, 0.8);
}
.score-display .value {
font-size: 16px;
color: #00ff41;
text-shadow:
-2px -2px 0 #000,
2px -2px 0 #000,
-2px 2px 0 #000,
2px 2px 0 #000,
0 0 12px rgba(0, 255, 65, 0.9);
}
.instructionStyle {
text-align: left;
background-color: #0a0a0a;
border: 4px solid #00ff00;
box-shadow:
0 0 10px #00ff00,
inset 0 0 10px rgba(0, 255, 0, 0.2);
padding: 5px;
margin: 5px auto;
color: #00ff00;
font-family: "Press Start 2P", cursive;
line-height: 1.5;
text-shadow: 2px 2px 0px #003300;
}
.instructionStyle {
background-color: #1a0033;
border: 4px solid #ff00ff;
box-shadow: 0 0 15px #ff00ff;
color: #ff00ff;
text-shadow: 2px 2px 0px #330033;
}
.instructionStyle p {
margin: 5px 0;
font-size: 10px;
}
.instructionStyle ul {
list-style: none;
padding-left: 5px;
margin: 5px 0;
}
.instructionStyle li {
margin: 5px 0;
font-size: 10px;
}
.instructionStyle li::before {
content: "► ";
color: #ff0000;
margin-right: 10px;
}
/* Style the jsPsych navigation buttons */
.jspsych-btn {
background-color: #ff00ff;
color: #1a0033;
border: 3px solid #ff00ff;
padding: 5px 10px;
font-family: "Press Start 2P", cursive;
font-size: 12px;
cursor: pointer;
text-transform: uppercase;
box-shadow: 4px 4px 0px #660066;
transition: all 0.1s;
margin: 5px;
}
.jspsych-btn:hover {
background-color: #cc00cc;
transform: translate(2px, 2px);
box-shadow: 2px 2px 0px #660066;
}
.jspsych-btn:active {
transform: translate(4px, 4px);
box-shadow: 0px 0px 0px #660066;
}
.jspsych-btn:disabled {
background-color: #333333;
color: #666666;
border-color: #666666;
box-shadow: none;
cursor: not-allowed;
}
/* Style the button container */
.jspsych-instructions-nav {
margin-top: 30px;
text-align: center;
}
.heartbeat {
-webkit-animation: heartbeat 1.5s ease-in-out infinite both;
animation: heartbeat 1.5s ease-in-out infinite both;
}
/* ----------------------------------------------
* Generated by Animista on 2025-10-24 16:10:42
* Licensed under FreeBSD License.
* See http://animista.net/license for more info.
* w: http://animista.net, t: @cssanimista
* ---------------------------------------------- */
/**
* ----------------------------------------
* animation heartbeat
* ----------------------------------------
*/
@-webkit-keyframes heartbeat {
from {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-transform-origin: center center;
transform-origin: center center;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
10% {
-webkit-transform: scale(0.91);
transform: scale(0.91);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
17% {
-webkit-transform: scale(0.98);
transform: scale(0.98);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
33% {
-webkit-transform: scale(0.87);
transform: scale(0.87);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
45% {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
}
@keyframes heartbeat {
from {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-transform-origin: center center;
transform-origin: center center;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
10% {
-webkit-transform: scale(0.91);
transform: scale(0.91);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
17% {
-webkit-transform: scale(0.98);
transform: scale(0.98);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
33% {
-webkit-transform: scale(0.87);
transform: scale(0.87);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
45% {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
}
.shake-horizontal {
-webkit-animation: shake-horizontal 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) both;
animation: shake-horizontal 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) both;
}
/* ----------------------------------------------
* Generated by Animista on 2025-10-24 16:11:44
* Licensed under FreeBSD License.
* See http://animista.net/license for more info.
* w: http://animista.net, t: @cssanimista
* ---------------------------------------------- */
/**
* ----------------------------------------
* animation shake-horizontal
* ----------------------------------------
*/
@-webkit-keyframes shake-horizontal {
0%,
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
10%,
30%,
50%,
70% {
-webkit-transform: translateX(-10px);
transform: translateX(-10px);
}
20%,
40%,
60% {
-webkit-transform: translateX(10px);
transform: translateX(10px);
}
80% {
-webkit-transform: translateX(8px);
transform: translateX(8px);
}
90% {
-webkit-transform: translateX(-8px);
transform: translateX(-8px);
}
}
@keyframes shake-horizontal {
0%,
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
10%,
30%,
50%,
70% {
-webkit-transform: translateX(-10px);
transform: translateX(-10px);
}
20%,
40%,
60% {
-webkit-transform: translateX(10px);
transform: translateX(10px);
}
80% {
-webkit-transform: translateX(8px);
transform: translateX(8px);
}
90% {
-webkit-transform: translateX(-8px);
transform: translateX(-8px);
}
}
26.4.2 Add Looping Background Music
Changes:
exp.js- Update the preload trial, update the audio setup, add music play/pause functions
<!DOCTYPE html>
<html>
<head>
<title>Lab 12: Implicit Learning</title>
<!-- jsPsych -->
<script src="jspsych/jspsych.js"></script>
<link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
<!-- jPsych plugins -->
<script src="jspsych/plugin-instructions.js"></script>
<script src="jspsych/plugin-preload.js"></script>
<script src="jspsych/plugin-html-keyboard-response.js"></script>
<!-- these are the google fonts -->
<link href="https://fonts.googleapis.com/css2?family=Creepster&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap"
rel="stylesheet">
<!-- custom CSS -->
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="game-display">
<div class="grid">
<div class="top-left">top-left</div>
<div class="top">top</div>
<div class="top-right">top-right</div>
<div class="left">left</div>
<div id="jspsych-game-display" class="center">center</div>
<div class="right">right</div>
<div class="bottom-left">bottom-left</div>
<div class="bottom">bottom</div>
<div class="bottom-right">bottom-right</div>
</div>
</div>
<!-- custom JS -->
<script src="exp.js"></script>
</body>
</html>
// ============================================
// Initiate jsPsych
// ============================================
const jsPsych = initJsPsych(
{display_element: 'jspsych-game-display'}
)
// ============================================
// Participant ID & Assign Condition
// ============================================
// Function to convert any string to a number
function stringToNumber(str) {
let sum = 0;
for (let i = 0; i < str.length; i++) {
sum += str.charCodeAt(i);
}
return sum;
}
// Get participant ID from URL, or generate random one if not found
let participant_id = jsPsych.data.getURLVariable("id") || jsPsych.randomization.randomInt(1, 100000).toString();
// Convert to number and assign condition
const numeric_id = stringToNumber(participant_id);
let conditions = ["implicit", "explicit"];
let assigned_condition = conditions[numeric_id % 2];
// Save id and assignment to data
jsPsych.data.addProperties({
participant_id: participant_id,
assigned_condition: assigned_condition
});
// ============================================
// Trackers
// ============================================
let score = 0
// ============================================
// Audio Setup
// ============================================
let sfx_coin = new Audio("audio/retro_coin.wav")
sfx_coin.volume = .3
let sfx_hit = new Audio("audio/retro_hit.wav")
sfx_hit.volume = .3
let sfx_squish = new Audio("audio/small_squish.mp3")
sfx_squish.volume = .1
let sfx_zombie = new Audio("audio/zombie-2.wav")
sfx_zombie.volume = .2
let bg_start = new Audio("audio/SeriousCutScene.wav")
bg_start.volume = .2
bg_start.loop = true
let bg_game = new Audio("audio/8BitMetal.wav")
bg_game.volume = .2
bg_game.loop = true
let bg_end = new Audio("audio/creep.mp3")
bg_end.volume = .2
bg_end.loop = true
// ============================================
// Preload
// ============================================
let preload = {
type: jsPsychPreload,
images: ["images/black.png", "images/bg.svg", "images/zombieAppear.png", "images/zombieIdle.png", "images/zombieDie.png", "images/zombieAttack.png"],
audio: ["audio/retro_coin.wav", "audio/retro_hit.wav", "audio/small_squish.mp3", "audio/zombie-2.wav",
"audio/SeriousCutScene.wav", "audio/8BitMetal.wav", "audio/creep.mp3"]
}
// ============================================
// Welcome
// ============================================
let welcome = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="instructionStyle">
<h3>PRESS ENTER TO START</h3>
</div>`,
choices: ["ENTER"]
}
// ============================================
// Instructions
// ============================================
let implicit_instructions = {
type: jsPsychInstructions,
pages: [`<div class="instructionStyle">
<p>Zombies are emerging from four graves in the cemetery!</p>
<p>Your mission: Whack them back down as quickly as possible using your keyboard:</p>
</div>`,
`<div class="instructionStyle">
<ul>
<li>Press "D" to whack the zombie in the first grave (leftmost)</li>
<li>Press "F" to whack the zombie in the second grave</li>
<li>Press "J" to whack the zombie in the third grave</li>
<li>Press "K" to whack the zombie in the fourth grave (rightmost)</li>
</ul>
</div>`,
`<div class="instructionStyle">
<p>Stay alert! Zombies will pop up randomly from different graves.</p>
<p>When you're ready to defend the cemetery, press NEXT</p>
</div>`],
show_clickable_nav: true,
on_start: function(){
bg_start.play();
},
on_finish: function(){
bg_start.pause();
bg_start.currentTime = 0;
}
}
let explicit_instructions = {
type: jsPsychInstructions,
pages: [`<div class="instructionStyle">
<p>Zombies are emerging from four graves in the cemetery!</p>
<p>Your mission: Whack them back down as quickly as possible using your keyboard:</p>
</div>`,
`<div class="instructionStyle">
<ul>
<li>Press "D" to whack the zombie in the first grave (leftmost)</li>
<li>Press "F" to whack the zombie in the second grave</li>
<li>Press "J" to whack the zombie in the third grave</li>
<li>Press "K" to whack the zombie in the fourth grave (rightmost)</li>
</ul>
</div>`,
`<div class="instructionStyle">
<p><strong>Secret Intel:</strong> These zombies follow a pattern!</p>
<p>The graves they emerge from repeat in a predictable sequence.</p>
<p>Your task is to learn this pattern while whacking zombies. The faster you learn it, the better you'll defend the cemetery!</p>
<p>When you're ready to begin your defense, press NEXT</p>
</div>`
],
show_clickable_nav: true,
on_start: function(){
bg_start.play();
},
on_finish: function(){
bg_start.pause();
bg_start.currentTime = 0;
}
}
// ============================================
// Implicit Learning Task
// ============================================
let blank = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`,
trial_duration: 300,
choices: "NO_KEYS",
data: {
trial_part: "blank"
}
}
let target = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let output
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
</div>`
}
return output
},
choices: ["d","f", "j", "k"],
data: {
trial_part: "target"
},
on_start: function(){
sfx_zombie.play();
}
}
let feedback = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let last_trial = jsPsych.data.get().last(1).values()
let last_response = last_trial[0].response
let last_accuracy = jsPsych.pluginAPI.compareKeys(last_response, jsPsych.evaluateTimelineVariable("correct_response"))
let feedback
if(last_accuracy){
feedback = "zombie-death"
score = score + 100
// play correct audio
sfx_coin.play();
sfx_squish.play();
} else {
feedback = "zombie-attack"
score = score - 100
// play incorrect audio
sfx_hit.play();
}
if(score < 0){score = 0}
// update score
document.querySelector("#score").innerHTML = score.toString().padStart(6, "0");
// add animation to score
if(last_accuracy){
document.querySelector("#score").classList.add("heartbeat")
} else {
document.querySelector("#score").classList.add("shake-horizontal")
}
// update output
let output
// still need the location
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
</div>`
}
return output
},
choices: "NO_KEYS",
trial_duration: 1200,
data: {
trial_part: "feedback"
},
on_finish: function(){
// make sure all audio has stopped and ready to start again
sfx_coin.pause();
sfx_hit.pause();
sfx_zombie.pause();
sfx_coin.currentTime = 0;
sfx_hit.currentTime = 0;
sfx_zombie.currentTime = 0;
// remove the animation classes
document.querySelector("#score").classList.remove("heartbeat")
document.querySelector("#score").classList.remove("shake-horizontal")
}
}
// learning phase in order
let learning = {
timeline: [
blank,
target,
feedback
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
repetitions: 1,
data: {
phase: "learning",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
},
on_timeline_start: function(){
bg_game.play();
}
}
// test phase reverses the order
let test = {
timeline: [
blank,
target,
feedback
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
sample: {
type: "custom",
fn: function(indices){
return indices.reverse(); // show the trials in the reverse order
}
},
repetitions: 2,
data: {
phase: "testing",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
},
on_timeline_finish: function(){
bg_game.pause();
bg_game.currentTime = 0;
}
}
// ============================================
// Savd Data Trial
// ============================================
const saveData = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `
<div class="instructionStyle" style="text-align: center">
<p>GAME OVER!</p>
<p>Click the button below to save your data:</p>
<button class="jspsych-btn">
SAVE DATA
</button>
</div>
`,
choices: "NO_KEYS",
trial_duration: null,
on_load: function() {
document.getElementById("save-btn").addEventListener("click", function() {
jsPsych.data.get().localSave("csv", "implicitLearning_data.csv");
});
},
data: {
phase: "save data trial"
},
on_start: function(){
bg_end.play();
},
on_finish: function(){
bg_end.pause();
bg_end.currentTime = 0;
}
};
// ============================================
// Run jsPsych
// ============================================
if(assigned_condition === "implicit"){
jsPsych.run([
preload,
welcome,
implicit_instructions,
learning,
test,
saveData
]);
} else if(assigned_condition === "explicit"){
jsPsych.run([
preload,
welcome,
explicit_instructions,
learning,
test,
saveData
]);
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.boxContainer {
/* width: 500px;
height: 300px; */
display: flex;
align-items: center;
justify-content: center;
}
.box {
width: 120px;
height: 150px;
/*border: solid;*/
margin: 0px 10px 0px 10px;
padding: 0;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background-image: url("images/dirt.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: contain;
}
/* GAME DISPLAY */
.game-display {
margin: 0 auto;
width: 720px;
height: 420px;
background-image: url("images/bg.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: cover;
}
.grid {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: .5fr repeat(4, 1fr) .5fr;
grid-template-rows: repeat(8, 1fr);
grid-template-areas:
"top-left top-left top top top-right top-right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"bottom-left bottom bottom bottom bottom bottom-right";
}
/* Grid area assignments */
.top-left { grid-area: top-left; }
.top-right { grid-area: top-right; }
.top { grid-area: top; }
.center { grid-area: center; }
.left { grid-area: left; }
.right { grid-area: right; }
.bottom-left { grid-area: bottom-left; }
.bottom-right { grid-area: bottom-right; }
.bottom { grid-area: bottom; }
/* add a border, so we can see the divs */
/*.grid > div {
border: 1px solid white;
margin: -.5px;
}
*/
.game-title {
font-size: 36px;
text-align: center;
font-family: "Creepster", cursive;
color: darkred;
letter-spacing: .07em;
transform: rotate(-2deg);
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 20px rgba(255, 215, 0, 0.8);
}
/* zombie animation classes */
.zombie-appear {
width: 115px;
height: 150px;
animation:
appear .3s steps(11) forwards,
idle 1s steps(6) .3s infinite;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes appear {
from {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: -1265px 0;
}
}
@keyframes idle {
from {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: 0 0;
}
to {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: -584px 0;
}
}
.zombie-death {
width: 120px;
height: 150px;
animation: death .6s steps(8) forwards;
background-repeat: no-repeat;
background-position: 0 0;
background-size: auto 150px;
margin-bottom: 40px;
}
@keyframes death {
from {
background-image: url("images/zombieDie.png");
background-position: 0 0;
}
to {
background-image: url("images/zombieDie.png");
background-position: -960px 0;
}
}
.zombie-attack {
width: 120px;
height: 150px;
animation:
attack .6s steps(7) forwards,
appear .6s steps(11) .6s forwards reverse;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes attack {
from {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: -840px 0;
}
}
/* Score displays */
.score-display {
height: 100%;
width: 100%;
font-family: "Press Start 2P", system-ui;
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
text-align: center;
}
.score-display .label {
font-size: 15px;
color: #ffd700;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 8px rgba(255, 215, 0, 0.8);
}
.score-display .value {
font-size: 16px;
color: #00ff41;
text-shadow:
-2px -2px 0 #000,
2px -2px 0 #000,
-2px 2px 0 #000,
2px 2px 0 #000,
0 0 12px rgba(0, 255, 65, 0.9);
}
.instructionStyle {
text-align: left;
background-color: #0a0a0a;
border: 4px solid #00ff00;
box-shadow:
0 0 10px #00ff00,
inset 0 0 10px rgba(0, 255, 0, 0.2);
padding: 5px;
margin: 5px auto;
color: #00ff00;
font-family: "Press Start 2P", cursive;
line-height: 1.5;
text-shadow: 2px 2px 0px #003300;
}
.instructionStyle {
background-color: #1a0033;
border: 4px solid #ff00ff;
box-shadow: 0 0 15px #ff00ff;
color: #ff00ff;
text-shadow: 2px 2px 0px #330033;
}
.instructionStyle p {
margin: 5px 0;
font-size: 10px;
}
.instructionStyle ul {
list-style: none;
padding-left: 5px;
margin: 5px 0;
}
.instructionStyle li {
margin: 5px 0;
font-size: 10px;
}
.instructionStyle li::before {
content: "► ";
color: #ff0000;
margin-right: 10px;
}
/* Style the jsPsych navigation buttons */
.jspsych-btn {
background-color: #ff00ff;
color: #1a0033;
border: 3px solid #ff00ff;
padding: 5px 10px;
font-family: "Press Start 2P", cursive;
font-size: 12px;
cursor: pointer;
text-transform: uppercase;
box-shadow: 4px 4px 0px #660066;
transition: all 0.1s;
margin: 5px;
}
.jspsych-btn:hover {
background-color: #cc00cc;
transform: translate(2px, 2px);
box-shadow: 2px 2px 0px #660066;
}
.jspsych-btn:active {
transform: translate(4px, 4px);
box-shadow: 0px 0px 0px #660066;
}
.jspsych-btn:disabled {
background-color: #333333;
color: #666666;
border-color: #666666;
box-shadow: none;
cursor: not-allowed;
}
/* Style the button container */
.jspsych-instructions-nav {
margin-top: 30px;
text-align: center;
}
.heartbeat {
-webkit-animation: heartbeat 1.5s ease-in-out infinite both;
animation: heartbeat 1.5s ease-in-out infinite both;
}
/* ----------------------------------------------
* Generated by Animista on 2025-10-24 16:10:42
* Licensed under FreeBSD License.
* See http://animista.net/license for more info.
* w: http://animista.net, t: @cssanimista
* ---------------------------------------------- */
/**
* ----------------------------------------
* animation heartbeat
* ----------------------------------------
*/
@-webkit-keyframes heartbeat {
from {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-transform-origin: center center;
transform-origin: center center;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
10% {
-webkit-transform: scale(0.91);
transform: scale(0.91);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
17% {
-webkit-transform: scale(0.98);
transform: scale(0.98);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
33% {
-webkit-transform: scale(0.87);
transform: scale(0.87);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
45% {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
}
@keyframes heartbeat {
from {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-transform-origin: center center;
transform-origin: center center;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
10% {
-webkit-transform: scale(0.91);
transform: scale(0.91);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
17% {
-webkit-transform: scale(0.98);
transform: scale(0.98);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
33% {
-webkit-transform: scale(0.87);
transform: scale(0.87);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
45% {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
}
.shake-horizontal {
-webkit-animation: shake-horizontal 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) both;
animation: shake-horizontal 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) both;
}
/* ----------------------------------------------
* Generated by Animista on 2025-10-24 16:11:44
* Licensed under FreeBSD License.
* See http://animista.net/license for more info.
* w: http://animista.net, t: @cssanimista
* ---------------------------------------------- */
/**
* ----------------------------------------
* animation shake-horizontal
* ----------------------------------------
*/
@-webkit-keyframes shake-horizontal {
0%,
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
10%,
30%,
50%,
70% {
-webkit-transform: translateX(-10px);
transform: translateX(-10px);
}
20%,
40%,
60% {
-webkit-transform: translateX(10px);
transform: translateX(10px);
}
80% {
-webkit-transform: translateX(8px);
transform: translateX(8px);
}
90% {
-webkit-transform: translateX(-8px);
transform: translateX(-8px);
}
}
@keyframes shake-horizontal {
0%,
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
10%,
30%,
50%,
70% {
-webkit-transform: translateX(-10px);
transform: translateX(-10px);
}
20%,
40%,
60% {
-webkit-transform: translateX(10px);
transform: translateX(10px);
}
80% {
-webkit-transform: translateX(8px);
transform: translateX(8px);
}
90% {
-webkit-transform: translateX(-8px);
transform: translateX(-8px);
}
}
26.5 Stage 3: Health, Death, and Restart
26.5.1 Add a Health Tracker
Changes:
index.html- update the HTML to include a hearts/health trackerexp.js- Update the trackers, update the feedback trial to lose health on hit, switch the shake animation to the health tracker instead of the score.style.css- add heart/health display styling
<!DOCTYPE html>
<html>
<head>
<title>Lab 12: Implicit Learning</title>
<!-- jsPsych -->
<script src="jspsych/jspsych.js"></script>
<link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
<!-- jPsych plugins -->
<script src="jspsych/plugin-instructions.js"></script>
<script src="jspsych/plugin-preload.js"></script>
<script src="jspsych/plugin-html-keyboard-response.js"></script>
<!-- these are the google fonts -->
<link href="https://fonts.googleapis.com/css2?family=Creepster&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap"
rel="stylesheet">
<!-- custom CSS -->
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="game-display">
<div class="grid">
<div class="top-left">
<div class="score-display">
<div class="label">SCORE</div>
<div id="score" class="value">000000</div>
</div>
</div>
<div class="top">
<p class="game-title">WHACK-A-ZOMBIE!</p>
</div>
<div class="top-right">
<div class="score-display">
<div class="label">HIGH-SCORE</div>
<div id="high-score" class="value">999999</div>
</div>
</div>
<div class="left">
<div class="lives-display">
<div class="hearts">
<span>❤️</span>
<span>❤️</span>
<span>❤️</span>
<span>❤️</span>
<span>❤️</span>
</div>
</div>
</div>
<div id="jspsych-game-display" class="center"></div>
<div class="right"></div>
<div class="bottom-left"></div>
<div class="bottom"></div>
<div class="bottom-right"></div>
</div>
</div>
<!-- custom JS -->
<script src="exp.js"></script>
</body>
</html>
// ============================================
// Initiate jsPsych
// ============================================
const jsPsych = initJsPsych(
{display_element: 'jspsych-game-display'}
)
// ============================================
// Participant ID & Assign Condition
// ============================================
// Function to convert any string to a number
function stringToNumber(str) {
let sum = 0;
for (let i = 0; i < str.length; i++) {
sum += str.charCodeAt(i);
}
return sum;
}
// Get participant ID from URL, or generate random one if not found
let participant_id = jsPsych.data.getURLVariable("id") || jsPsych.randomization.randomInt(1, 100000).toString();
// Convert to number and assign condition
const numeric_id = stringToNumber(participant_id);
let conditions = ["implicit", "explicit"];
let assigned_condition = conditions[numeric_id % 2];
// Save id and assignment to data
jsPsych.data.addProperties({
participant_id: participant_id,
assigned_condition: assigned_condition
});
// ============================================
// Trackers
// ============================================
let score = 0
let hearts = 5
// ============================================
// Audio Setup
// ============================================
let sfx_coin = new Audio("audio/retro_coin.wav")
sfx_coin.volume = .3
let sfx_hit = new Audio("audio/retro_hit.wav")
sfx_hit.volume = .3
let sfx_squish = new Audio("audio/small_squish.mp3")
sfx_squish.volume = .1
let sfx_zombie = new Audio("audio/zombie-2.wav")
sfx_zombie.volume = .2
let bg_start = new Audio("audio/SeriousCutScene.wav")
bg_start.volume = .2
bg_start.loop = true
let bg_game = new Audio("audio/8BitMetal.wav")
bg_game.volume = .2
bg_game.loop = true
let bg_end = new Audio("audio/creep.mp3")
bg_end.volume = .2
bg_end.loop = true
// ============================================
// Preload
// ============================================
let preload = {
type: jsPsychPreload,
images: ["images/black.png", "images/bg.svg", "images/zombieAppear.png", "images/zombieIdle.png", "images/zombieDie.png", "images/zombieAttack.png"],
audio: ["audio/retro_coin.wav", "audio/retro_hit.wav", "audio/small_squish.mp3", "audio/zombie-2.wav",
"audio/SeriousCutScene.wav", "audio/8BitMetal.wav", "audio/creep.mp3"]
}
// ============================================
// Welcome
// ============================================
let welcome = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="instructionStyle">
<h3>PRESS ENTER TO START</h3>
</div>`,
choices: ["ENTER"]
}
// ============================================
// Instructions
// ============================================
let implicit_instructions = {
type: jsPsychInstructions,
pages: [`<div class="instructionStyle">
<p>Zombies are emerging from four graves in the cemetery!</p>
<p>Your mission: Whack them back down as quickly as possible using your keyboard:</p>
</div>`,
`<div class="instructionStyle">
<ul>
<li>Press "D" to whack the zombie in the first grave (leftmost)</li>
<li>Press "F" to whack the zombie in the second grave</li>
<li>Press "J" to whack the zombie in the third grave</li>
<li>Press "K" to whack the zombie in the fourth grave (rightmost)</li>
</ul>
</div>`,
`<div class="instructionStyle">
<p>Stay alert! Zombies will pop up randomly from different graves.</p>
<p>When you're ready to defend the cemetery, press NEXT</p>
</div>`],
show_clickable_nav: true,
on_start: function(){
bg_start.play();
},
on_finish: function(){
bg_start.pause();
bg_start.currentTime = 0;
}
}
let explicit_instructions = {
type: jsPsychInstructions,
pages: [`<div class="instructionStyle">
<p>Zombies are emerging from four graves in the cemetery!</p>
<p>Your mission: Whack them back down as quickly as possible using your keyboard:</p>
</div>`,
`<div class="instructionStyle">
<ul>
<li>Press "D" to whack the zombie in the first grave (leftmost)</li>
<li>Press "F" to whack the zombie in the second grave</li>
<li>Press "J" to whack the zombie in the third grave</li>
<li>Press "K" to whack the zombie in the fourth grave (rightmost)</li>
</ul>
</div>`,
`<div class="instructionStyle">
<p><strong>Secret Intel:</strong> These zombies follow a pattern!</p>
<p>The graves they emerge from repeat in a predictable sequence.</p>
<p>Your task is to learn this pattern while whacking zombies. The faster you learn it, the better you'll defend the cemetery!</p>
<p>When you're ready to begin your defense, press NEXT</p>
</div>`
],
show_clickable_nav: true,
on_start: function(){
bg_start.play();
},
on_finish: function(){
bg_start.pause();
bg_start.currentTime = 0;
}
}
// ============================================
// Implicit Learning Task
// ============================================
let blank = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`,
trial_duration: 300,
choices: "NO_KEYS",
data: {
trial_part: "blank"
}
}
let target = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let output
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
</div>`
}
return output
},
choices: ["d","f", "j", "k"],
data: {
trial_part: "target"
},
on_start: function(){
sfx_zombie.play();
}
}
let feedback = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let last_trial = jsPsych.data.get().last(1).values()
let last_response = last_trial[0].response
let last_accuracy = jsPsych.pluginAPI.compareKeys(last_response, jsPsych.evaluateTimelineVariable("correct_response"))
let feedback
if(last_accuracy){
feedback = "zombie-death"
score = score + 100
// play correct audio
sfx_coin.play();
sfx_squish.play();
} else {
feedback = "zombie-attack"
//score = score - 100
hearts = hearts - 1
let heart_display = document.querySelector(".hearts")
if(hearts == 5){
heart_display.innerHTML = `<span>❤️</span><span>❤️</span><span>❤️</span><span>❤️</span><span>❤️</span>`
} else if(hearts == 4){
heart_display.innerHTML = `<span>❤️</span><span>❤️</span><span>❤️</span><span>❤️</span><span>🖤</span>`
} else if(hearts == 3){
heart_display.innerHTML = `<span>❤️</span><span>❤️</span><span>❤️</span><span>🖤</span><span>🖤</span>`
} else if(hearts == 2){
heart_display.innerHTML = `<span>❤️</span><span>❤️</span><span>🖤</span><span>🖤</span><span>🖤</span>`
} else if(hearts == 1){
heart_display.innerHTML = `<span>❤️</span><span>🖤</span><span>🖤</span><span>🖤</span><span>🖤</span>`
} else if(hearts == 0){
heart_display.innerHTML = `<span>🖤</span><span>🖤</span><span>🖤</span><span>🖤</span><span>🖤</span>`
}
// play incorrect audio
sfx_hit.play();
}
if(score < 0){score = 0}
// update score
document.querySelector("#score").innerHTML = score.toString().padStart(6, "0");
// add animation to score
if(last_accuracy){
document.querySelector("#score").classList.add("heartbeat")
} else {
document.querySelector(".hearts").classList.add("shake-horizontal")
}
// update output
let output
// still need the location
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
</div>`
}
return output
},
choices: "NO_KEYS",
trial_duration: 1200,
data: {
trial_part: "feedback"
},
on_finish: function(){
// make sure all audio has stopped and ready to start again
sfx_coin.pause();
sfx_hit.pause();
sfx_zombie.pause();
sfx_coin.currentTime = 0;
sfx_hit.currentTime = 0;
sfx_zombie.currentTime = 0;
// remove the animation classes
document.querySelector("#score").classList.remove("heartbeat")
document.querySelector(".hearts").classList.remove("shake-horizontal")
}
}
// learning phase in order
let learning = {
timeline: [
blank,
target,
feedback
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
repetitions: 1,
data: {
phase: "learning",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
},
on_timeline_start: function(){
bg_game.play();
}
}
// test phase reverses the order
let test = {
timeline: [
blank,
target,
feedback
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
sample: {
type: "custom",
fn: function(indices){
return indices.reverse(); // show the trials in the reverse order
}
},
repetitions: 2,
data: {
phase: "testing",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
},
on_timeline_finish: function(){
bg_game.pause();
bg_game.currentTime = 0;
}
}
// ============================================
// Savd Data Trial
// ============================================
const saveData = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `
<div class="instructionStyle" style="text-align: center">
<p>GAME OVER!</p>
<p>Click the button below to save your data:</p>
<button class="jspsych-btn">
SAVE DATA
</button>
</div>
`,
choices: "NO_KEYS",
trial_duration: null,
on_load: function() {
document.getElementById("save-btn").addEventListener("click", function() {
jsPsych.data.get().localSave("csv", "implicitLearning_data.csv");
});
},
data: {
phase: "save data trial"
},
on_start: function(){
bg_end.play();
},
on_finish: function(){
bg_end.pause();
bg_end.currentTime = 0;
}
};
// ============================================
// Run jsPsych
// ============================================
if(assigned_condition === "implicit"){
jsPsych.run([
preload,
welcome,
implicit_instructions,
learning,
test,
saveData
]);
} else if(assigned_condition === "explicit"){
jsPsych.run([
preload,
welcome,
explicit_instructions,
learning,
test,
saveData
]);
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.boxContainer {
/* width: 500px;
height: 300px; */
display: flex;
align-items: center;
justify-content: center;
}
.box {
width: 120px;
height: 150px;
/*border: solid;*/
margin: 0px 10px 0px 10px;
padding: 0;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background-image: url("images/dirt.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: contain;
}
/* GAME DISPLAY */
.game-display {
margin: 0 auto;
width: 720px;
height: 420px;
background-image: url("images/bg.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: cover;
}
.grid {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: .5fr repeat(4, 1fr) .5fr;
grid-template-rows: repeat(8, 1fr);
grid-template-areas:
"top-left top-left top top top-right top-right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"bottom-left bottom bottom bottom bottom bottom-right";
}
/* Grid area assignments */
.top-left { grid-area: top-left; }
.top-right { grid-area: top-right; }
.top { grid-area: top; }
.center { grid-area: center; }
.left { grid-area: left; }
.right { grid-area: right; }
.bottom-left { grid-area: bottom-left; }
.bottom-right { grid-area: bottom-right; }
.bottom { grid-area: bottom; }
/* add a border, so we can see the divs */
/*.grid > div {
border: 1px solid white;
margin: -.5px;
}
*/
.game-title {
font-size: 36px;
text-align: center;
font-family: "Creepster", cursive;
color: darkred;
letter-spacing: .07em;
transform: rotate(-2deg);
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 20px rgba(255, 215, 0, 0.8);
}
/* zombie animation classes */
.zombie-appear {
width: 115px;
height: 150px;
animation:
appear .3s steps(11) forwards,
idle 1s steps(6) .3s infinite;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes appear {
from {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: -1265px 0;
}
}
@keyframes idle {
from {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: 0 0;
}
to {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: -584px 0;
}
}
.zombie-death {
width: 120px;
height: 150px;
animation: death .6s steps(8) forwards;
background-repeat: no-repeat;
background-position: 0 0;
background-size: auto 150px;
margin-bottom: 40px;
}
@keyframes death {
from {
background-image: url("images/zombieDie.png");
background-position: 0 0;
}
to {
background-image: url("images/zombieDie.png");
background-position: -960px 0;
}
}
.zombie-attack {
width: 120px;
height: 150px;
animation:
attack .6s steps(7) forwards,
appear .6s steps(11) .6s forwards reverse;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes attack {
from {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: -840px 0;
}
}
/* Score displays */
.score-display {
height: 100%;
width: 100%;
font-family: "Press Start 2P", system-ui;
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
text-align: center;
}
.score-display .label {
font-size: 15px;
color: #ffd700;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 8px rgba(255, 215, 0, 0.8);
}
.score-display .value {
font-size: 16px;
color: #00ff41;
text-shadow:
-2px -2px 0 #000,
2px -2px 0 #000,
-2px 2px 0 #000,
2px 2px 0 #000,
0 0 12px rgba(0, 255, 65, 0.9);
}
/* Lives display */
.lives-display {
font-family: "Press Start 2P", system-ui;
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
}
.hearts {
font-size: 16px;
filter: drop-shadow(0 0 5px #ff0066);
display: flex;
flex-direction: column;
gap: 5px;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 8px rgba(255, 215, 0, 0.8);
}
.instructionStyle {
text-align: left;
background-color: #0a0a0a;
border: 4px solid #00ff00;
box-shadow:
0 0 10px #00ff00,
inset 0 0 10px rgba(0, 255, 0, 0.2);
padding: 5px;
margin: 5px auto;
color: #00ff00;
font-family: "Press Start 2P", cursive;
line-height: 1.5;
text-shadow: 2px 2px 0px #003300;
}
.instructionStyle {
background-color: #1a0033;
border: 4px solid #ff00ff;
box-shadow: 0 0 15px #ff00ff;
color: #ff00ff;
text-shadow: 2px 2px 0px #330033;
}
.instructionStyle p {
margin: 5px 0;
font-size: 10px;
}
.instructionStyle ul {
list-style: none;
padding-left: 5px;
margin: 5px 0;
}
.instructionStyle li {
margin: 5px 0;
font-size: 10px;
}
.instructionStyle li::before {
content: "► ";
color: #ff0000;
margin-right: 10px;
}
/* Style the jsPsych navigation buttons */
.jspsych-btn {
background-color: #ff00ff;
color: #1a0033;
border: 3px solid #ff00ff;
padding: 5px 10px;
font-family: "Press Start 2P", cursive;
font-size: 12px;
cursor: pointer;
text-transform: uppercase;
box-shadow: 4px 4px 0px #660066;
transition: all 0.1s;
margin: 5px;
}
.jspsych-btn:hover {
background-color: #cc00cc;
transform: translate(2px, 2px);
box-shadow: 2px 2px 0px #660066;
}
.jspsych-btn:active {
transform: translate(4px, 4px);
box-shadow: 0px 0px 0px #660066;
}
.jspsych-btn:disabled {
background-color: #333333;
color: #666666;
border-color: #666666;
box-shadow: none;
cursor: not-allowed;
}
/* Style the button container */
.jspsych-instructions-nav {
margin-top: 30px;
text-align: center;
}
.heartbeat {
-webkit-animation: heartbeat 1.5s ease-in-out infinite both;
animation: heartbeat 1.5s ease-in-out infinite both;
}
/* ----------------------------------------------
* Generated by Animista on 2025-10-24 16:10:42
* Licensed under FreeBSD License.
* See http://animista.net/license for more info.
* w: http://animista.net, t: @cssanimista
* ---------------------------------------------- */
/**
* ----------------------------------------
* animation heartbeat
* ----------------------------------------
*/
@-webkit-keyframes heartbeat {
from {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-transform-origin: center center;
transform-origin: center center;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
10% {
-webkit-transform: scale(0.91);
transform: scale(0.91);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
17% {
-webkit-transform: scale(0.98);
transform: scale(0.98);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
33% {
-webkit-transform: scale(0.87);
transform: scale(0.87);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
45% {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
}
@keyframes heartbeat {
from {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-transform-origin: center center;
transform-origin: center center;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
10% {
-webkit-transform: scale(0.91);
transform: scale(0.91);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
17% {
-webkit-transform: scale(0.98);
transform: scale(0.98);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
33% {
-webkit-transform: scale(0.87);
transform: scale(0.87);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
45% {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
}
.shake-horizontal {
-webkit-animation: shake-horizontal 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) both;
animation: shake-horizontal 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) both;
}
/* ----------------------------------------------
* Generated by Animista on 2025-10-24 16:11:44
* Licensed under FreeBSD License.
* See http://animista.net/license for more info.
* w: http://animista.net, t: @cssanimista
* ---------------------------------------------- */
/**
* ----------------------------------------
* animation shake-horizontal
* ----------------------------------------
*/
@-webkit-keyframes shake-horizontal {
0%,
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
10%,
30%,
50%,
70% {
-webkit-transform: translateX(-10px);
transform: translateX(-10px);
}
20%,
40%,
60% {
-webkit-transform: translateX(10px);
transform: translateX(10px);
}
80% {
-webkit-transform: translateX(8px);
transform: translateX(8px);
}
90% {
-webkit-transform: translateX(-8px);
transform: translateX(-8px);
}
}
@keyframes shake-horizontal {
0%,
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
10%,
30%,
50%,
70% {
-webkit-transform: translateX(-10px);
transform: translateX(-10px);
}
20%,
40%,
60% {
-webkit-transform: translateX(10px);
transform: translateX(10px);
}
80% {
-webkit-transform: translateX(8px);
transform: translateX(8px);
}
90% {
-webkit-transform: translateX(-8px);
transform: translateX(-8px);
}
}
26.5.2 Add a Death/Restart Screen
Another gameplay we could add is adding a ‘game over’ death if they lose all their hearts. We’ll make use of the conditional function to check for death on every trial and display the end screen if hearts reaches 0.
To restart the game, I had to end the experiment, clear the display, and restart everything.
Changes:
exp.js- Add a new conditional trial that occurs after death, add the death trial to the learning and test timelines
<!DOCTYPE html>
<html>
<head>
<title>Lab 12: Implicit Learning</title>
<!-- jsPsych -->
<script src="jspsych/jspsych.js"></script>
<link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
<!-- jPsych plugins -->
<script src="jspsych/plugin-instructions.js"></script>
<script src="jspsych/plugin-preload.js"></script>
<script src="jspsych/plugin-html-keyboard-response.js"></script>
<!-- these are the google fonts -->
<link href="https://fonts.googleapis.com/css2?family=Creepster&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap"
rel="stylesheet">
<!-- custom CSS -->
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="game-display">
<div class="grid">
<div class="top-left">
<div class="score-display">
<div class="label">SCORE</div>
<div id="score" class="value">000000</div>
</div>
</div>
<div class="top">
<p class="game-title">WHACK-A-ZOMBIE!</p>
</div>
<div class="top-right">
<div class="score-display">
<div class="label">HIGH-SCORE</div>
<div id="high-score" class="value">999999</div>
</div>
</div>
<div class="left">
<div class="lives-display">
<div class="hearts">
<span>❤️</span>
<span>❤️</span>
<span>❤️</span>
<span>❤️</span>
<span>❤️</span>
</div>
</div>
</div>
<div id="jspsych-game-display" class="center"></div>
<div class="right"></div>
<div class="bottom-left"></div>
<div class="bottom"></div>
<div class="bottom-right"></div>
</div>
</div>
<!-- custom JS -->
<script src="exp.js"></script>
</body>
</html>
// ============================================
// Initiate jsPsych
// ============================================
const jsPsych = initJsPsych(
{display_element: 'jspsych-game-display'}
)
// ============================================
// Participant ID & Assign Condition
// ============================================
// Function to convert any string to a number
function stringToNumber(str) {
let sum = 0;
for (let i = 0; i < str.length; i++) {
sum += str.charCodeAt(i);
}
return sum;
}
// Get participant ID from URL, or generate random one if not found
let participant_id = jsPsych.data.getURLVariable("id") || jsPsych.randomization.randomInt(1, 100000).toString();
// Convert to number and assign condition
const numeric_id = stringToNumber(participant_id);
let conditions = ["implicit", "explicit"];
let assigned_condition = conditions[numeric_id % 2];
// Save id and assignment to data
jsPsych.data.addProperties({
participant_id: participant_id,
assigned_condition: assigned_condition
});
// ============================================
// Trackers
// ============================================
let score = 0
let hearts = 5
// ============================================
// Audio Setup
// ============================================
let sfx_coin = new Audio("audio/retro_coin.wav")
sfx_coin.volume = .3
let sfx_hit = new Audio("audio/retro_hit.wav")
sfx_hit.volume = .3
let sfx_squish = new Audio("audio/small_squish.mp3")
sfx_squish.volume = .1
let sfx_zombie = new Audio("audio/zombie-2.wav")
sfx_zombie.volume = .2
let bg_start = new Audio("audio/SeriousCutScene.wav")
bg_start.volume = .2
bg_start.loop = true
let bg_game = new Audio("audio/8BitMetal.wav")
bg_game.volume = .2
bg_game.loop = true
let bg_end = new Audio("audio/creep.mp3")
bg_end.volume = .2
bg_end.loop = true
// ============================================
// Preload
// ============================================
let preload = {
type: jsPsychPreload,
images: ["images/black.png", "images/bg.svg", "images/zombieAppear.png", "images/zombieIdle.png", "images/zombieDie.png", "images/zombieAttack.png"],
audio: ["audio/retro_coin.wav", "audio/retro_hit.wav", "audio/small_squish.mp3", "audio/zombie-2.wav",
"audio/SeriousCutScene.wav", "audio/8BitMetal.wav", "audio/creep.mp3"]
}
// ============================================
// Welcome
// ============================================
let welcome = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="instructionStyle">
<h3>PRESS ENTER TO START</h3>
</div>`,
choices: ["ENTER"]
}
// ============================================
// Instructions
// ============================================
let implicit_instructions = {
type: jsPsychInstructions,
pages: [`<div class="instructionStyle">
<p>Zombies are emerging from four graves in the cemetery!</p>
<p>Your mission: Whack them back down as quickly as possible using your keyboard:</p>
</div>`,
`<div class="instructionStyle">
<ul>
<li>Press "D" to whack the zombie in the first grave (leftmost)</li>
<li>Press "F" to whack the zombie in the second grave</li>
<li>Press "J" to whack the zombie in the third grave</li>
<li>Press "K" to whack the zombie in the fourth grave (rightmost)</li>
</ul>
</div>`,
`<div class="instructionStyle">
<p>Stay alert! Zombies will pop up randomly from different graves.</p>
<p>When you're ready to defend the cemetery, press NEXT</p>
</div>`],
show_clickable_nav: true,
on_start: function(){
bg_start.play();
},
on_finish: function(){
bg_start.pause();
bg_start.currentTime = 0;
}
}
let explicit_instructions = {
type: jsPsychInstructions,
pages: [`<div class="instructionStyle">
<p>Zombies are emerging from four graves in the cemetery!</p>
<p>Your mission: Whack them back down as quickly as possible using your keyboard:</p>
</div>`,
`<div class="instructionStyle">
<ul>
<li>Press "D" to whack the zombie in the first grave (leftmost)</li>
<li>Press "F" to whack the zombie in the second grave</li>
<li>Press "J" to whack the zombie in the third grave</li>
<li>Press "K" to whack the zombie in the fourth grave (rightmost)</li>
</ul>
</div>`,
`<div class="instructionStyle">
<p><strong>Secret Intel:</strong> These zombies follow a pattern!</p>
<p>The graves they emerge from repeat in a predictable sequence.</p>
<p>Your task is to learn this pattern while whacking zombies. The faster you learn it, the better you'll defend the cemetery!</p>
<p>When you're ready to begin your defense, press NEXT</p>
</div>`
],
show_clickable_nav: true,
on_start: function(){
bg_start.play();
},
on_finish: function(){
bg_start.pause();
bg_start.currentTime = 0;
}
}
// ============================================
// Implicit Learning Task
// ============================================
let blank = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`,
trial_duration: 300,
choices: "NO_KEYS",
data: {
trial_part: "blank"
}
}
let target = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let output
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
</div>`
}
return output
},
choices: ["d","f", "j", "k"],
data: {
trial_part: "target"
},
on_start: function(){
sfx_zombie.play();
}
}
let feedback = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let last_trial = jsPsych.data.get().last(1).values()
let last_response = last_trial[0].response
let last_accuracy = jsPsych.pluginAPI.compareKeys(last_response, jsPsych.evaluateTimelineVariable("correct_response"))
let feedback
if(last_accuracy){
feedback = "zombie-death"
score = score + 100
// play correct audio
sfx_coin.play();
sfx_squish.play();
} else {
feedback = "zombie-attack"
//score = score - 100
hearts = hearts - 1
let heart_display = document.querySelector(".hearts")
if(hearts == 5){
heart_display.innerHTML = `<span>❤️</span><span>❤️</span><span>❤️</span><span>❤️</span><span>❤️</span>`
} else if(hearts == 4){
heart_display.innerHTML = `<span>❤️</span><span>❤️</span><span>❤️</span><span>❤️</span><span>🖤</span>`
} else if(hearts == 3){
heart_display.innerHTML = `<span>❤️</span><span>❤️</span><span>❤️</span><span>🖤</span><span>🖤</span>`
} else if(hearts == 2){
heart_display.innerHTML = `<span>❤️</span><span>❤️</span><span>🖤</span><span>🖤</span><span>🖤</span>`
} else if(hearts == 1){
heart_display.innerHTML = `<span>❤️</span><span>🖤</span><span>🖤</span><span>🖤</span><span>🖤</span>`
} else if(hearts == 0){
heart_display.innerHTML = `<span>🖤</span><span>🖤</span><span>🖤</span><span>🖤</span><span>🖤</span>`
}
// play incorrect audio
sfx_hit.play();
}
if(score < 0){score = 0}
// update score
document.querySelector("#score").innerHTML = score.toString().padStart(6, "0");
// add animation to score
if(last_accuracy){
document.querySelector("#score").classList.add("heartbeat")
} else {
document.querySelector(".hearts").classList.add("shake-horizontal")
}
// update output
let output
// still need the location
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
</div>`
}
return output
},
choices: "NO_KEYS",
trial_duration: 1200,
data: {
trial_part: "feedback"
},
on_finish: function(){
// make sure all audio has stopped and ready to start again
sfx_coin.pause();
sfx_hit.pause();
sfx_zombie.pause();
sfx_coin.currentTime = 0;
sfx_hit.currentTime = 0;
sfx_zombie.currentTime = 0;
// remove the animation classes
document.querySelector("#score").classList.remove("heartbeat")
document.querySelector(".hearts").classList.remove("shake-horizontal")
}
}
// check death
let death = {
timeline: [
{
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="instructionStyle" style="text-align: center">
<h3 class="game-title" style="color:#ff00ff">You Died!</h3>
<p>Press "ENTER" to try again.</p>
</div>`,
choices: ["ENTER"],
trial_duration: null,
data: {trial_part: "death"},
on_start: function(){
bg_game.pause();
bg_end.play();
},
on_finish: function(){
bg_end.pause();
}
}
],
conditional_function: function(){
// this timeline runs IF hearts is 0 or less
if(hearts <= 0){
return true;
} else {
return false;
}
},
on_timeline_finish: function(){
// pause all music
bg_game.pause();
bg_game.currentTime = 0;
bg_start.pause();
bg_start.currentTime = 0;
bg_end.pause();
bg_end.currentTime = 0;
// end jsPsych early
jsPsych.abortExperiment();
// clear the jspsych display display
document.querySelector("#jspsych-game-display").innerHTML = ""
// restart the score/hearts
document.querySelector(".hearts").innerHTML = `<span>❤️</span><span>❤️</span><span>❤️</span><span>❤️</span><span>❤️</span>`
document.querySelector("#score").innerHTML = "000000"
score = 0
hearts = 5
// restart jspsych
if(assigned_condition === "implicit"){
jsPsych.run([
implicit_instructions,
learning,
test,
saveData
]);
} else if(assigned_condition === "explicit"){
jsPsych.run([
explicit_instructions,
learning,
test,
saveData
]);
}
}
}
// learning phase in order
let learning = {
timeline: [
blank,
target,
feedback,
death
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
repetitions: 1,
data: {
phase: "learning",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
},
on_timeline_start: function(){
bg_game.play();
}
}
// test phase reverses the order
let test = {
timeline: [
blank,
target,
feedback,
death
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
sample: {
type: "custom",
fn: function(indices){
return indices.reverse(); // show the trials in the reverse order
}
},
repetitions: 2,
data: {
phase: "testing",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
},
on_timeline_finish: function(){
bg_game.pause();
bg_game.currentTime = 0;
}
}
// ============================================
// Savd Data Trial
// ============================================
const saveData = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `
<div class="instructionStyle" style="text-align: center">
<p>GAME OVER!</p>
<p>Click the button below to save your data:</p>
<button class="jspsych-btn">
SAVE DATA
</button>
</div>
`,
choices: "NO_KEYS",
trial_duration: null,
on_load: function() {
document.getElementById("save-btn").addEventListener("click", function() {
jsPsych.data.get().localSave("csv", "implicitLearning_data.csv");
});
},
data: {
phase: "save data trial"
},
on_start: function(){
bg_end.play();
},
on_finish: function(){
bg_end.pause();
bg_end.currentTime = 0;
}
};
// ============================================
// Run jsPsych
// ============================================
if(assigned_condition === "implicit"){
jsPsych.run([
preload,
welcome,
implicit_instructions,
learning,
test,
saveData
]);
} else if(assigned_condition === "explicit"){
jsPsych.run([
preload,
welcome,
explicit_instructions,
learning,
test,
saveData
]);
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.boxContainer {
/* width: 500px;
height: 300px; */
display: flex;
align-items: center;
justify-content: center;
}
.box {
width: 120px;
height: 150px;
/*border: solid;*/
margin: 0px 10px 0px 10px;
padding: 0;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background-image: url("images/dirt.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: contain;
}
/* GAME DISPLAY */
.game-display {
margin: 0 auto;
width: 720px;
height: 420px;
background-image: url("images/bg.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: cover;
}
.grid {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: .5fr repeat(4, 1fr) .5fr;
grid-template-rows: repeat(8, 1fr);
grid-template-areas:
"top-left top-left top top top-right top-right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"bottom-left bottom bottom bottom bottom bottom-right";
}
/* Grid area assignments */
.top-left { grid-area: top-left; }
.top-right { grid-area: top-right; }
.top { grid-area: top; }
.center { grid-area: center; }
.left { grid-area: left; }
.right { grid-area: right; }
.bottom-left { grid-area: bottom-left; }
.bottom-right { grid-area: bottom-right; }
.bottom { grid-area: bottom; }
/* add a border, so we can see the divs */
/*.grid > div {
border: 1px solid white;
margin: -.5px;
}
*/
.game-title {
font-size: 36px;
text-align: center;
font-family: "Creepster", cursive;
color: darkred;
letter-spacing: .07em;
transform: rotate(-2deg);
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 20px rgba(255, 215, 0, 0.8);
}
/* zombie animation classes */
.zombie-appear {
width: 115px;
height: 150px;
animation:
appear .3s steps(11) forwards,
idle 1s steps(6) .3s infinite;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes appear {
from {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: -1265px 0;
}
}
@keyframes idle {
from {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: 0 0;
}
to {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: -584px 0;
}
}
.zombie-death {
width: 120px;
height: 150px;
animation: death .6s steps(8) forwards;
background-repeat: no-repeat;
background-position: 0 0;
background-size: auto 150px;
margin-bottom: 40px;
}
@keyframes death {
from {
background-image: url("images/zombieDie.png");
background-position: 0 0;
}
to {
background-image: url("images/zombieDie.png");
background-position: -960px 0;
}
}
.zombie-attack {
width: 120px;
height: 150px;
animation:
attack .6s steps(7) forwards,
appear .6s steps(11) .6s forwards reverse;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes attack {
from {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: -840px 0;
}
}
/* Score displays */
.score-display {
height: 100%;
width: 100%;
font-family: "Press Start 2P", system-ui;
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
text-align: center;
}
.score-display .label {
font-size: 15px;
color: #ffd700;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 8px rgba(255, 215, 0, 0.8);
}
.score-display .value {
font-size: 16px;
color: #00ff41;
text-shadow:
-2px -2px 0 #000,
2px -2px 0 #000,
-2px 2px 0 #000,
2px 2px 0 #000,
0 0 12px rgba(0, 255, 65, 0.9);
}
/* Lives display */
.lives-display {
font-family: "Press Start 2P", system-ui;
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
}
.hearts {
font-size: 16px;
filter: drop-shadow(0 0 5px #ff0066);
display: flex;
flex-direction: column;
gap: 5px;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 8px rgba(255, 215, 0, 0.8);
}
.instructionStyle {
text-align: left;
background-color: #0a0a0a;
border: 4px solid #00ff00;
box-shadow:
0 0 10px #00ff00,
inset 0 0 10px rgba(0, 255, 0, 0.2);
padding: 5px;
margin: 5px auto;
color: #00ff00;
font-family: "Press Start 2P", cursive;
line-height: 1.5;
text-shadow: 2px 2px 0px #003300;
}
.instructionStyle {
background-color: #1a0033;
border: 4px solid #ff00ff;
box-shadow: 0 0 15px #ff00ff;
color: #ff00ff;
text-shadow: 2px 2px 0px #330033;
}
.instructionStyle p {
margin: 5px 0;
font-size: 10px;
}
.instructionStyle ul {
list-style: none;
padding-left: 5px;
margin: 5px 0;
}
.instructionStyle li {
margin: 5px 0;
font-size: 10px;
}
.instructionStyle li::before {
content: "► ";
color: #ff0000;
margin-right: 10px;
}
/* Style the jsPsych navigation buttons */
.jspsych-btn {
background-color: #ff00ff;
color: #1a0033;
border: 3px solid #ff00ff;
padding: 5px 10px;
font-family: "Press Start 2P", cursive;
font-size: 12px;
cursor: pointer;
text-transform: uppercase;
box-shadow: 4px 4px 0px #660066;
transition: all 0.1s;
margin: 5px;
}
.jspsych-btn:hover {
background-color: #cc00cc;
transform: translate(2px, 2px);
box-shadow: 2px 2px 0px #660066;
}
.jspsych-btn:active {
transform: translate(4px, 4px);
box-shadow: 0px 0px 0px #660066;
}
.jspsych-btn:disabled {
background-color: #333333;
color: #666666;
border-color: #666666;
box-shadow: none;
cursor: not-allowed;
}
/* Style the button container */
.jspsych-instructions-nav {
margin-top: 30px;
text-align: center;
}
.heartbeat {
-webkit-animation: heartbeat 1.5s ease-in-out infinite both;
animation: heartbeat 1.5s ease-in-out infinite both;
}
/* ----------------------------------------------
* Generated by Animista on 2025-10-24 16:10:42
* Licensed under FreeBSD License.
* See http://animista.net/license for more info.
* w: http://animista.net, t: @cssanimista
* ---------------------------------------------- */
/**
* ----------------------------------------
* animation heartbeat
* ----------------------------------------
*/
@-webkit-keyframes heartbeat {
from {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-transform-origin: center center;
transform-origin: center center;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
10% {
-webkit-transform: scale(0.91);
transform: scale(0.91);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
17% {
-webkit-transform: scale(0.98);
transform: scale(0.98);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
33% {
-webkit-transform: scale(0.87);
transform: scale(0.87);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
45% {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
}
@keyframes heartbeat {
from {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-transform-origin: center center;
transform-origin: center center;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
10% {
-webkit-transform: scale(0.91);
transform: scale(0.91);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
17% {
-webkit-transform: scale(0.98);
transform: scale(0.98);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
33% {
-webkit-transform: scale(0.87);
transform: scale(0.87);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
45% {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
}
.shake-horizontal {
-webkit-animation: shake-horizontal 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) both;
animation: shake-horizontal 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) both;
}
/* ----------------------------------------------
* Generated by Animista on 2025-10-24 16:11:44
* Licensed under FreeBSD License.
* See http://animista.net/license for more info.
* w: http://animista.net, t: @cssanimista
* ---------------------------------------------- */
/**
* ----------------------------------------
* animation shake-horizontal
* ----------------------------------------
*/
@-webkit-keyframes shake-horizontal {
0%,
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
10%,
30%,
50%,
70% {
-webkit-transform: translateX(-10px);
transform: translateX(-10px);
}
20%,
40%,
60% {
-webkit-transform: translateX(10px);
transform: translateX(10px);
}
80% {
-webkit-transform: translateX(8px);
transform: translateX(8px);
}
90% {
-webkit-transform: translateX(-8px);
transform: translateX(-8px);
}
}
@keyframes shake-horizontal {
0%,
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
10%,
30%,
50%,
70% {
-webkit-transform: translateX(-10px);
transform: translateX(-10px);
}
20%,
40%,
60% {
-webkit-transform: translateX(10px);
transform: translateX(10px);
}
80% {
-webkit-transform: translateX(8px);
transform: translateX(8px);
}
90% {
-webkit-transform: translateX(-8px);
transform: translateX(-8px);
}
}
26.5.3 Update the High Score
A final gameplay mechanic I’ll add is to make the high score track each individual’s personal high score.
Changes:
index.html- change the starting high score to 0exp.js- add a high score tracker, add logic to update the high score
<!DOCTYPE html>
<html>
<head>
<title>Lab 12: Implicit Learning</title>
<!-- jsPsych -->
<script src="jspsych/jspsych.js"></script>
<link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
<!-- jPsych plugins -->
<script src="jspsych/plugin-instructions.js"></script>
<script src="jspsych/plugin-preload.js"></script>
<script src="jspsych/plugin-html-keyboard-response.js"></script>
<!-- these are the google fonts -->
<link href="https://fonts.googleapis.com/css2?family=Creepster&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap"
rel="stylesheet">
<!-- custom CSS -->
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="game-display">
<div class="grid">
<div class="top-left">
<div class="score-display">
<div class="label">SCORE</div>
<div id="score" class="value">000000</div>
</div>
</div>
<div class="top">
<p class="game-title">WHACK-A-ZOMBIE!</p>
</div>
<div class="top-right">
<div class="score-display">
<div class="label">HIGH-SCORE</div>
<div id="high-score" class="value">000000</div>
</div>
</div>
<div class="left">
<div class="lives-display">
<div class="hearts">
<span>❤️</span>
<span>❤️</span>
<span>❤️</span>
<span>❤️</span>
<span>❤️</span>
</div>
</div>
</div>
<div id="jspsych-game-display" class="center"></div>
<div class="right"></div>
<div class="bottom-left"></div>
<div class="bottom"></div>
<div class="bottom-right"></div>
</div>
</div>
<!-- custom JS -->
<script src="exp.js"></script>
</body>
</html>
// ============================================
// Initiate jsPsych
// ============================================
const jsPsych = initJsPsych(
{display_element: 'jspsych-game-display'}
)
// ============================================
// Participant ID & Assign Condition
// ============================================
// Function to convert any string to a number
function stringToNumber(str) {
let sum = 0;
for (let i = 0; i < str.length; i++) {
sum += str.charCodeAt(i);
}
return sum;
}
// Get participant ID from URL, or generate random one if not found
let participant_id = jsPsych.data.getURLVariable("id") || jsPsych.randomization.randomInt(1, 100000).toString();
// Convert to number and assign condition
const numeric_id = stringToNumber(participant_id);
let conditions = ["implicit", "explicit"];
let assigned_condition = conditions[numeric_id % 2];
// Save id and assignment to data
jsPsych.data.addProperties({
participant_id: participant_id,
assigned_condition: assigned_condition
});
// ============================================
// Trackers
// ============================================
let score = 0
let hearts = 5
let high_score = 0
// ============================================
// Audio Setup
// ============================================
let sfx_coin = new Audio("audio/retro_coin.wav")
sfx_coin.volume = .3
let sfx_hit = new Audio("audio/retro_hit.wav")
sfx_hit.volume = .3
let sfx_squish = new Audio("audio/small_squish.mp3")
sfx_squish.volume = .1
let sfx_zombie = new Audio("audio/zombie-2.wav")
sfx_zombie.volume = .2
let bg_start = new Audio("audio/SeriousCutScene.wav")
bg_start.volume = .2
bg_start.loop = true
let bg_game = new Audio("audio/8BitMetal.wav")
bg_game.volume = .2
bg_game.loop = true
let bg_end = new Audio("audio/creep.mp3")
bg_end.volume = .2
bg_end.loop = true
// ============================================
// Preload
// ============================================
let preload = {
type: jsPsychPreload,
images: ["images/black.png", "images/bg.svg", "images/zombieAppear.png", "images/zombieIdle.png", "images/zombieDie.png", "images/zombieAttack.png"],
audio: ["audio/retro_coin.wav", "audio/retro_hit.wav", "audio/small_squish.mp3", "audio/zombie-2.wav",
"audio/SeriousCutScene.wav", "audio/8BitMetal.wav", "audio/creep.mp3"]
}
// ============================================
// Welcome
// ============================================
let welcome = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="instructionStyle">
<h3>PRESS ENTER TO START</h3>
</div>`,
choices: ["ENTER"]
}
// ============================================
// Instructions
// ============================================
let implicit_instructions = {
type: jsPsychInstructions,
pages: [`<div class="instructionStyle">
<p>Zombies are emerging from four graves in the cemetery!</p>
<p>Your mission: Whack them back down as quickly as possible using your keyboard:</p>
</div>`,
`<div class="instructionStyle">
<ul>
<li>Press "D" to whack the zombie in the first grave (leftmost)</li>
<li>Press "F" to whack the zombie in the second grave</li>
<li>Press "J" to whack the zombie in the third grave</li>
<li>Press "K" to whack the zombie in the fourth grave (rightmost)</li>
</ul>
</div>`,
`<div class="instructionStyle">
<p>Stay alert! Zombies will pop up randomly from different graves.</p>
<p>When you're ready to defend the cemetery, press NEXT</p>
</div>`],
show_clickable_nav: true,
on_start: function(){
bg_start.play();
},
on_finish: function(){
bg_start.pause();
bg_start.currentTime = 0;
}
}
let explicit_instructions = {
type: jsPsychInstructions,
pages: [`<div class="instructionStyle">
<p>Zombies are emerging from four graves in the cemetery!</p>
<p>Your mission: Whack them back down as quickly as possible using your keyboard:</p>
</div>`,
`<div class="instructionStyle">
<ul>
<li>Press "D" to whack the zombie in the first grave (leftmost)</li>
<li>Press "F" to whack the zombie in the second grave</li>
<li>Press "J" to whack the zombie in the third grave</li>
<li>Press "K" to whack the zombie in the fourth grave (rightmost)</li>
</ul>
</div>`,
`<div class="instructionStyle">
<p><strong>Secret Intel:</strong> These zombies follow a pattern!</p>
<p>The graves they emerge from repeat in a predictable sequence.</p>
<p>Your task is to learn this pattern while whacking zombies. The faster you learn it, the better you'll defend the cemetery!</p>
<p>When you're ready to begin your defense, press NEXT</p>
</div>`
],
show_clickable_nav: true,
on_start: function(){
bg_start.play();
},
on_finish: function(){
bg_start.pause();
bg_start.currentTime = 0;
}
}
// ============================================
// Implicit Learning Task
// ============================================
let blank = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`,
trial_duration: 300,
choices: "NO_KEYS",
data: {
trial_part: "blank"
}
}
let target = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let output
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="zombie-appear"></div></div>
</div>`
}
return output
},
choices: ["d","f", "j", "k"],
data: {
trial_part: "target"
},
on_start: function(){
sfx_zombie.play();
}
}
let feedback = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function(){
let last_trial = jsPsych.data.get().last(1).values()
let last_response = last_trial[0].response
let last_accuracy = jsPsych.pluginAPI.compareKeys(last_response, jsPsych.evaluateTimelineVariable("correct_response"))
let feedback
if(last_accuracy){
feedback = "zombie-death"
score = score + 100
// play correct audio
sfx_coin.play();
sfx_squish.play();
} else {
feedback = "zombie-attack"
//score = score - 100
hearts = hearts - 1
let heart_display = document.querySelector(".hearts")
if(hearts == 5){
heart_display.innerHTML = `<span>❤️</span><span>❤️</span><span>❤️</span><span>❤️</span><span>❤️</span>`
} else if(hearts == 4){
heart_display.innerHTML = `<span>❤️</span><span>❤️</span><span>❤️</span><span>❤️</span><span>🖤</span>`
} else if(hearts == 3){
heart_display.innerHTML = `<span>❤️</span><span>❤️</span><span>❤️</span><span>🖤</span><span>🖤</span>`
} else if(hearts == 2){
heart_display.innerHTML = `<span>❤️</span><span>❤️</span><span>🖤</span><span>🖤</span><span>🖤</span>`
} else if(hearts == 1){
heart_display.innerHTML = `<span>❤️</span><span>🖤</span><span>🖤</span><span>🖤</span><span>🖤</span>`
} else if(hearts == 0){
heart_display.innerHTML = `<span>🖤</span><span>🖤</span><span>🖤</span><span>🖤</span><span>🖤</span>`
}
// play incorrect audio
sfx_hit.play();
}
if(score < 0){score = 0}
// update score
document.querySelector("#score").innerHTML = score.toString().padStart(6, "0");
// add animation to score
if(last_accuracy){
document.querySelector("#score").classList.add("heartbeat")
} else {
document.querySelector(".hearts").classList.add("shake-horizontal")
}
//update high score
if(score > high_score){
high_score = score
document.querySelector("#high-score").innerHTML = high_score.toString().padStart(6, "0");
document.querySelector("#high-score").classList.add("heartbeat")
}
// update output
let output
// still need the location
let target_location = jsPsych.evaluateTimelineVariable("location")
if(target_location == 1){
output = `<div class="boxContainer">
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 2){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
<div class="box"></div>
</div>`
} else if(target_location == 3){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
<div class="box"></div>
</div>`
} else if(target_location == 4){
output = `<div class="boxContainer">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"><div class="${feedback}"></div></div>
</div>`
}
return output
},
choices: "NO_KEYS",
trial_duration: 1200,
data: {
trial_part: "feedback"
},
on_finish: function(){
// make sure all audio has stopped and ready to start again
sfx_coin.pause();
sfx_hit.pause();
sfx_zombie.pause();
sfx_coin.currentTime = 0;
sfx_hit.currentTime = 0;
sfx_zombie.currentTime = 0;
// remove the animation classes
document.querySelector("#score").classList.remove("heartbeat")
document.querySelector(".hearts").classList.remove("shake-horizontal")
document.querySelector("#high-score").classList.remove("heartbeat")
}
}
// check death
let death = {
timeline: [
{
type: jsPsychHtmlKeyboardResponse,
stimulus: `<div class="instructionStyle" style="text-align: center">
<h3 class="game-title" style="color:#ff00ff">You Died!</h3>
<p>Press "ENTER" to try again.</p>
</div>`,
choices: ["ENTER"],
trial_duration: null,
data: {trial_part: "death"},
on_start: function(){
bg_game.pause();
bg_end.play();
},
on_finish: function(){
bg_end.pause();
}
}
],
conditional_function: function(){
// this timeline runs IF hearts is 0 or less
if(hearts <= 0){
return true;
} else {
return false;
}
},
on_timeline_finish: function(){
// pause all music
bg_game.pause();
bg_game.currentTime = 0;
bg_start.pause();
bg_start.currentTime = 0;
bg_end.pause();
bg_end.currentTime = 0;
// end jsPsych early
jsPsych.abortExperiment();
// clear the jspsych display display
document.querySelector("#jspsych-game-display").innerHTML = ""
// restart the score/hearts
document.querySelector(".hearts").innerHTML = `<span>❤️</span><span>❤️</span><span>❤️</span><span>❤️</span><span>❤️</span>`
document.querySelector("#score").innerHTML = "000000"
score = 0
hearts = 5
// restart jspsych
if(assigned_condition === "implicit"){
jsPsych.run([
implicit_instructions,
learning,
test,
saveData
]);
} else if(assigned_condition === "explicit"){
jsPsych.run([
explicit_instructions,
learning,
test,
saveData
]);
}
}
}
// learning phase in order
let learning = {
timeline: [
blank,
target,
feedback,
death
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
repetitions: 1,
data: {
phase: "learning",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
},
on_timeline_start: function(){
bg_game.play();
}
}
// test phase reverses the order
let test = {
timeline: [
blank,
target,
feedback,
death
],
timeline_variables: [
{location: "1", correct_response: "d"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "1", correct_response: "d"},
{location: "3", correct_response: "j"},
{location: "2", correct_response: "f"},
{location: "4", correct_response: "k"},
{location: "3", correct_response: "j"}
],
sample: {
type: "custom",
fn: function(indices){
return indices.reverse(); // show the trials in the reverse order
}
},
repetitions: 2,
data: {
phase: "testing",
location: jsPsych.timelineVariable("location"),
correct_response: jsPsych.timelineVariable("correct_response")
},
on_timeline_finish: function(){
bg_game.pause();
bg_game.currentTime = 0;
}
}
// ============================================
// Savd Data Trial
// ============================================
const saveData = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `
<div class="instructionStyle" style="text-align: center">
<p>GAME OVER!</p>
<p>Click the button below to save your data:</p>
<button class="jspsych-btn">
SAVE DATA
</button>
</div>
`,
choices: "NO_KEYS",
trial_duration: null,
on_load: function() {
document.getElementById("save-btn").addEventListener("click", function() {
jsPsych.data.get().localSave("csv", "implicitLearning_data.csv");
});
},
data: {
phase: "save data trial"
},
on_start: function(){
bg_end.play();
},
on_finish: function(){
bg_end.pause();
bg_end.currentTime = 0;
}
};
// ============================================
// Run jsPsych
// ============================================
if(assigned_condition === "implicit"){
jsPsych.run([
preload,
welcome,
implicit_instructions,
learning,
test,
saveData
]);
} else if(assigned_condition === "explicit"){
jsPsych.run([
preload,
welcome,
explicit_instructions,
learning,
test,
saveData
]);
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.boxContainer {
/* width: 500px;
height: 300px; */
display: flex;
align-items: center;
justify-content: center;
}
.box {
width: 120px;
height: 150px;
/*border: solid;*/
margin: 0px 10px 0px 10px;
padding: 0;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background-image: url("images/dirt.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: contain;
}
/* GAME DISPLAY */
.game-display {
margin: 0 auto;
width: 720px;
height: 420px;
background-image: url("images/bg.svg");
background-repeat: no-repeat;
background-position: bottom;
background-size: cover;
}
.grid {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: .5fr repeat(4, 1fr) .5fr;
grid-template-rows: repeat(8, 1fr);
grid-template-areas:
"top-left top-left top top top-right top-right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"left center center center center right"
"bottom-left bottom bottom bottom bottom bottom-right";
}
/* Grid area assignments */
.top-left { grid-area: top-left; }
.top-right { grid-area: top-right; }
.top { grid-area: top; }
.center { grid-area: center; }
.left { grid-area: left; }
.right { grid-area: right; }
.bottom-left { grid-area: bottom-left; }
.bottom-right { grid-area: bottom-right; }
.bottom { grid-area: bottom; }
/* add a border, so we can see the divs */
/*.grid > div {
border: 1px solid white;
margin: -.5px;
}
*/
.game-title {
font-size: 36px;
text-align: center;
font-family: "Creepster", cursive;
color: darkred;
letter-spacing: .07em;
transform: rotate(-2deg);
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 20px rgba(255, 215, 0, 0.8);
}
/* zombie animation classes */
.zombie-appear {
width: 115px;
height: 150px;
animation:
appear .3s steps(11) forwards,
idle 1s steps(6) .3s infinite;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes appear {
from {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAppear.png");
background-position: -1265px 0;
}
}
@keyframes idle {
from {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: 0 0;
}
to {
width: 97px;
background-image: url("images/zombieIdle.png");
background-position: -584px 0;
}
}
.zombie-death {
width: 120px;
height: 150px;
animation: death .6s steps(8) forwards;
background-repeat: no-repeat;
background-position: 0 0;
background-size: auto 150px;
margin-bottom: 40px;
}
@keyframes death {
from {
background-image: url("images/zombieDie.png");
background-position: 0 0;
}
to {
background-image: url("images/zombieDie.png");
background-position: -960px 0;
}
}
.zombie-attack {
width: 120px;
height: 150px;
animation:
attack .6s steps(7) forwards,
appear .6s steps(11) .6s forwards reverse;
background-repeat: no-repeat;
background-position: center bottom 40px;
background-size: auto 150px; /* Maintains sprite sheet aspect ratio */
margin-bottom: 40px;
}
@keyframes attack {
from {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: 0 0;
}
to {
width: 120px;
background-image: url("images/zombieAttack.png");
background-position: -840px 0;
}
}
/* Score displays */
.score-display {
height: 100%;
width: 100%;
font-family: "Press Start 2P", system-ui;
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
text-align: center;
}
.score-display .label {
font-size: 15px;
color: #ffd700;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 8px rgba(255, 215, 0, 0.8);
}
.score-display .value {
font-size: 16px;
color: #00ff41;
text-shadow:
-2px -2px 0 #000,
2px -2px 0 #000,
-2px 2px 0 #000,
2px 2px 0 #000,
0 0 12px rgba(0, 255, 65, 0.9);
}
/* Lives display */
.lives-display {
font-family: "Press Start 2P", system-ui;
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
}
.hearts {
font-size: 16px;
filter: drop-shadow(0 0 5px #ff0066);
display: flex;
flex-direction: column;
gap: 5px;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 8px rgba(255, 215, 0, 0.8);
}
.instructionStyle {
text-align: left;
background-color: #0a0a0a;
border: 4px solid #00ff00;
box-shadow:
0 0 10px #00ff00,
inset 0 0 10px rgba(0, 255, 0, 0.2);
padding: 5px;
margin: 5px auto;
color: #00ff00;
font-family: "Press Start 2P", cursive;
line-height: 1.5;
text-shadow: 2px 2px 0px #003300;
}
.instructionStyle {
background-color: #1a0033;
border: 4px solid #ff00ff;
box-shadow: 0 0 15px #ff00ff;
color: #ff00ff;
text-shadow: 2px 2px 0px #330033;
}
.instructionStyle p {
margin: 5px 0;
font-size: 10px;
}
.instructionStyle ul {
list-style: none;
padding-left: 5px;
margin: 5px 0;
}
.instructionStyle li {
margin: 5px 0;
font-size: 10px;
}
.instructionStyle li::before {
content: "► ";
color: #ff0000;
margin-right: 10px;
}
/* Style the jsPsych navigation buttons */
.jspsych-btn {
background-color: #ff00ff;
color: #1a0033;
border: 3px solid #ff00ff;
padding: 5px 10px;
font-family: "Press Start 2P", cursive;
font-size: 12px;
cursor: pointer;
text-transform: uppercase;
box-shadow: 4px 4px 0px #660066;
transition: all 0.1s;
margin: 5px;
}
.jspsych-btn:hover {
background-color: #cc00cc;
transform: translate(2px, 2px);
box-shadow: 2px 2px 0px #660066;
}
.jspsych-btn:active {
transform: translate(4px, 4px);
box-shadow: 0px 0px 0px #660066;
}
.jspsych-btn:disabled {
background-color: #333333;
color: #666666;
border-color: #666666;
box-shadow: none;
cursor: not-allowed;
}
/* Style the button container */
.jspsych-instructions-nav {
margin-top: 30px;
text-align: center;
}
.heartbeat {
-webkit-animation: heartbeat 1.5s ease-in-out infinite both;
animation: heartbeat 1.5s ease-in-out infinite both;
}
/* ----------------------------------------------
* Generated by Animista on 2025-10-24 16:10:42
* Licensed under FreeBSD License.
* See http://animista.net/license for more info.
* w: http://animista.net, t: @cssanimista
* ---------------------------------------------- */
/**
* ----------------------------------------
* animation heartbeat
* ----------------------------------------
*/
@-webkit-keyframes heartbeat {
from {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-transform-origin: center center;
transform-origin: center center;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
10% {
-webkit-transform: scale(0.91);
transform: scale(0.91);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
17% {
-webkit-transform: scale(0.98);
transform: scale(0.98);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
33% {
-webkit-transform: scale(0.87);
transform: scale(0.87);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
45% {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
}
@keyframes heartbeat {
from {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-transform-origin: center center;
transform-origin: center center;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
10% {
-webkit-transform: scale(0.91);
transform: scale(0.91);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
17% {
-webkit-transform: scale(0.98);
transform: scale(0.98);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
33% {
-webkit-transform: scale(0.87);
transform: scale(0.87);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
45% {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
}
.shake-horizontal {
-webkit-animation: shake-horizontal 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) both;
animation: shake-horizontal 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) both;
}
/* ----------------------------------------------
* Generated by Animista on 2025-10-24 16:11:44
* Licensed under FreeBSD License.
* See http://animista.net/license for more info.
* w: http://animista.net, t: @cssanimista
* ---------------------------------------------- */
/**
* ----------------------------------------
* animation shake-horizontal
* ----------------------------------------
*/
@-webkit-keyframes shake-horizontal {
0%,
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
10%,
30%,
50%,
70% {
-webkit-transform: translateX(-10px);
transform: translateX(-10px);
}
20%,
40%,
60% {
-webkit-transform: translateX(10px);
transform: translateX(10px);
}
80% {
-webkit-transform: translateX(8px);
transform: translateX(8px);
}
90% {
-webkit-transform: translateX(-8px);
transform: translateX(-8px);
}
}
@keyframes shake-horizontal {
0%,
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
10%,
30%,
50%,
70% {
-webkit-transform: translateX(-10px);
transform: translateX(-10px);
}
20%,
40%,
60% {
-webkit-transform: translateX(10px);
transform: translateX(10px);
}
80% {
-webkit-transform: translateX(8px);
transform: translateX(8px);
}
90% {
-webkit-transform: translateX(-8px);
transform: translateX(-8px);
}
}