Leave a Reply

Your email address will not be published. Required fields are marked *

Documents

Leave a Reply

Your email address will not be published. Required fields are marked *

We have started to use the AI Revision Bot across the Department of ITDD at Exeter College to support learner’s revision. Overall the feedback is very positive – apart from “It gives a lot of feedback” which strikes me as “I can’t be bothered to read it” more than an actual critique. In response to colleague’s feedback there have also been improvements in functionality and the UI.

Here is the Student User Manual:

Here is the Admin Manual:

Contact me: simonrundell@exe-coll.ac.uk if you would like to implement this (free) resource. I licence it under Creative Commons CC NC-BY-SA 4.0

Leave a Reply

Your email address will not be published. Required fields are marked *

Leave a Reply

Your email address will not be published. Required fields are marked *

This is a student-safe environment to learn and tinker with SQL queries without the complexities of setup or the risk of compromising shared data. All processing happens in a virtual database simulation.

FREE for any student, teacher or class to use in your teaching at: https://sqlsim.toolsforteaching.co.uk under CC NC-BY-SA licence NC BY-NC-SA

This is the main screen

Leave a Reply

Your email address will not be published. Required fields are marked *

Leave a Reply

Your email address will not be published. Required fields are marked *

Leave a Reply

Your email address will not be published. Required fields are marked *

Leave a Reply

Your email address will not be published. Required fields are marked *

Leave a Reply

Your email address will not be published. Required fields are marked *

I usually develop Office extensions in response to a given need. My mentor suggested that an on-screen timer would help keep the time limit in my student’s mind when they undertook an activity. There are many web-based solutions available, such as https://classroomscreen.com and sometimes there are timers you can call up on the Smart TV display (although not consistantly).

There are no easy solutions for keeping it within PowerPoint. The age of the ActiveX control is dead due to security concerns, so the only viable solution within PowerPoint is a VBA only solution. There are a number of technical issues with this (look away now if you are a teacher looking for a solution and not a Computer Science teacher):

  • There is no inbuilt timer within PowerPoint, so you have to use a Windows system timer. This is why this solution has to be Windows-only, sorry.
  • Assigning Macros back and forth between compiled .ppam (PowerPoint Add-ins) and the Template is unreliable and relies on code inside the Template which is not always possible.
  • The solution is to manage it all in the .ppam and add a “Live Manager” dialog box which remains on screen and is used to set and fire off the timer.

(non-technical teachers can come back now)

The function is part of my Macro Extension CodeMonkey_PPT.ppam

When that extension is installed as a PowerPoint Extension, a new toolbar is visible and there is an option to “Run Live” at the right

This opens up a dialog box and enables you to start your presentation. It remains on top of the presenter view while you are teaching.

When you get to a slide where you want to have a timer, you select the time (between 30 seconds and 60 minutes) and hit Start

The timer runs for the required time and makes a system sound when the time is up

You can continue with the slide whilst it is running. If you have a slide which follows which already has a timer on it, and move to that slide the timer continues running. This means with a little preparation you can run a time over 2 or more slides!

I plan to release this shortly. Let me know if you are interested.

Leave a Reply

Your email address will not be published. Required fields are marked *

Leave a Reply

Your email address will not be published. Required fields are marked *

Leave a Reply

Your email address will not be published. Required fields are marked *

Leave a Reply

Your email address will not be published. Required fields are marked *

One of the things that is really overwhelming for an ECT is the amount of Planning you need to get together. More experienced teachers tell me it gets much better after the first 2 years because you have material in the bag to modify, but for now it is all about scheduling new stuff and getting your head around it all.

So, of course I need a spreadsheet and of course it has automation. This is a macro-enabled excel spreadsheet so Excel will ask you if you want to “enable content”. You do, because my code doesn’t work without it.

In the template sheet, organise your timetable and codes as you need. There is a custom ribbon bar at the top under “CodeMonkey”. Pressing the Insert New Sheets button gives you a dialog box

Choose a Monday and a new sheet will be created based upon the content of “Template” and named by the date chosen in YYYY-MM-DD format, which is then sorted into date order.

Adopt, Adapt. Improve. Simon Rundell CC NC-BY-SA 2025

Leave a Reply

Your email address will not be published. Required fields are marked *

Leave a Reply

Your email address will not be published. Required fields are marked *

We all know the benefits of recall of previous learning, especially when spaced as we move information from working into long term memory

For this reason, I start all my lessons with a multiple choice quiz on Microsoft Forms. To do this efficiently, I use ChatGPT to analyse my previous lesson slides (perhaps 2 or 3 past lessons for spaced recall) using this prompt:

The text is this:

Based upon this these teaching slides for 2025 Pearson T-Level Level 3 in Digital Software Development, please can you create five multiple choice questions for recall testing. The last option of each question should be I don't know. Please also indicate the correct answer. Please create the answer in a word document for which I can use the Quick Import feature in Microsoft Forms. I understand the format of the questions should be:

1. Question
A. Option 1
B. Option 2
C. Option 3
D. Option 4
ANSWER: D
POINT: 1

All I do is then check the import, ensure every question is required and copy the shortened link.

I put the link on my first slide and email it to my students to arrive in time for the beginning of the lesson. They can do this while I am calling the register.

It works and it saves a serious amount of time.

Leave a Reply

Your email address will not be published. Required fields are marked *

Leave a Reply

Your email address will not be published. Required fields are marked *

Leave a Reply

Your email address will not be published. Required fields are marked *

Leave a Reply

Your email address will not be published. Required fields are marked *

Leave a Reply

Your email address will not be published. Required fields are marked *

[document_library id=”575″]

Lesson 1 requires bread, butter and jam, knives, paper plates and a cloth (which I forgot, note to self – don’t forget that again)

With much love and respect to Randall Monroe xkcd.com

Leave a Reply

Your email address will not be published. Required fields are marked *

Leave a Reply

Your email address will not be published. Required fields are marked *

Further Kallbak Improvements: searchable insertion of questions inside PowerPoint

I decided to make the UI of the PowerPoint element of Kallbak better, so enabled the facility to see all the questions and be able to search and find a question, then with a single button press, the question is entered into the default template. All you have to do then is style it as your school desires or it meets your own peronal preferences. Even the QR Code is automatically generated for you.

Kallbak is free for educators. I’m using it myself, so why not have a try yourself? Visit www.kallbak.com and register for free.

Leave a Reply

Your email address will not be published. Required fields are marked *

Kallbak now features Likert Scale Questions

Likert Scales are the 1-5 rating scales used all over the place. They have a long history in research questionnaires and have been seriously over-used by web marketeer, but they are really useful in understanding by how much someone feels.

There is a place for this kind of question in the classroom so I have built in a configurable option for Likert Scales.

You can have as many options on the scale as you like, but it is usually an odd number. Given the size of a mobile screen this means in reality a maximum of 7.

When displayed on the screen, responses arrive in real-time as before much like a multiple choice. If anyone would like a different kind of display for a Likert Scale then please contact me and I will try and implement it.

This is the PowerPoint view

Leave a Reply

Your email address will not be published. Required fields are marked *

Kallback Powerpoint Module: Improved Wordcloud

Whereas creating a wordcloud within a web-based App was relatively easy, creating that inside PowerPoint (one of the key features of Kallbak) was quite challenging, and the code inside PowerPoint was not as good as the web-based one.

However, that has been vastly improved with release 1.2 of the CodeMonkey Kallbak add-on for PowerPoint which connects to my own self-written API endpoint and delivers a wordcloud graphic on demand.

The PowerPoint Add-in can be obtained at: https://www.kallbak.com/download once you have registered (for free!) and installed as a PowerPoint Add in.

The WordCloudAPI is written in NodeJS and therefore released under the MIT Licence and can be found on my Github at https://github.com/SimonRundell/WordCloudAPI should you wish to adopt, adapt and improve it or host it on your own webserver. There is also a demonstration of a client there to access it on a web page: https://github.com/SimonRundell/WordCloudAPI

Leave a Reply

Your email address will not be published. Required fields are marked *

Form multiple drives into a single mountable drive on Ubuntu

This is a script which does the above. I have three additional drives on my Ubuntu box which I wanted to amalgamate into a single mount for my media resources for sharing. I could have done it manually, one drive at a time, but why not spend hours working on a bash script that does it in less than a minute and I could use again (someday) or you might want to be able to use it for your own media server.

#!/bin/bash
set -e

# === CONFIGURATION ===
DISKS=("/dev/sdb" "/dev/sdc" "/dev/sdd")
VG_NAME="mediavg"
LV_NAME="medialv"
MOUNT_POINT="/mnt/media"
FS_TYPE="ext4"

echo "=== STEP 1: Wiping existing data and creating GPT partitions ==="
for disk in "${DISKS[@]}"; do
    echo "Wiping and partitioning $disk"
    sudo wipefs -a "$disk"
    sudo parted -s "$disk" mklabel gpt
    sudo parted -s "$disk" mkpart primary 0% 100%
done

sleep 2  # Let the kernel re-read the partition table

# Identify partitions (assumes each drive has one partition now)
PARTITIONS=("${DISKS[@]/%/1}")

echo "=== STEP 2: Creating LVM Physical Volumes ==="
for part in "${PARTITIONS[@]}"; do
    echo "Creating PV on $part"
    sudo pvcreate --force --yes "$part"
done

echo "=== STEP 3: Creating Volume Group ($VG_NAME) ==="
sudo vgcreate "$VG_NAME" "${PARTITIONS[@]}"

echo "=== STEP 4: Creating Logical Volume ($LV_NAME) ==="
sudo lvcreate -l 100%FREE -n "$LV_NAME" "$VG_NAME"

echo "=== STEP 5: Creating $FS_TYPE filesystem ==="
sudo mkfs.$FS_TYPE "/dev/$VG_NAME/$LV_NAME"

echo "=== STEP 6: Creating and mounting to $MOUNT_POINT ==="
sudo mkdir -p "$MOUNT_POINT"
sudo mount "/dev/$VG_NAME/$LV_NAME" "$MOUNT_POINT"

echo "=== STEP 7: Adding to /etc/fstab for persistence ==="
UUID=$(sudo blkid -s UUID -o value "/dev/$VG_NAME/$LV_NAME")
echo "UUID=$UUID $MOUNT_POINT $FS_TYPE defaults 0 2" | sudo tee -a /etc/fstab

echo "Done: Your media volume is mounted at $MOUNT_POINT"

Copy and paste this to a text file with nano:

nano setup_media_pool.sh

Then make it executable:

chmod +x setup_media_pool.sh

To run it, type:

sudo ./setup_media_pool.sh

…and it will set it upo for you. If you don’t have 3 extra drives, remove sdc and sdd, but you must realise that this operation will delete all the data on these drives so make sure you want to do this as this cannot be gone back on.

Then I configured Samba file sharing with the following smb.conf so that only machines in my local network 192.168.0.* could access it.

[global]
   server string = Media Server
   workgroup = WORKGROUP
   netbios name = MEDIASERV
   server role = standalone server
   map to guest = Bad User
   log file = /var/log/samba/log.%m
   max log size = 1000
   dns proxy = no

   # Restrict access to local network only (optional but recommended)
   interfaces = lo enp0s3 192.168.0.0/24
   bind interfaces only = yes

   # Enable guest access
   security = user
   guest account = nobody
   usershare allow guests = yes

# Disable unused services
   load printers = no
   disable spoolss = yes
   printing = bsd

# Media Share
[MEDIA]
   comment = Shared Media Storage
   path = /mnt/media
   browseable = yes
   writable = yes
   guest ok = yes
   public = yes
   create mask = 0775
   directory mask = 0775
   force user = nobody
   force group = nogroup

Test the configuration first:

sudo testparm
sudo systemctl restart smbd

and then check the permissions of the shared folder:

sudo chown -R nobody:nogroup /mnt/media
sudo chmod -R 775 /mnt/media

I did have to create a Samba user to log in as:

sudo smbpasswd -a smbsharer

…and then set a password, after which you enable it with the following:

sudo smbpasswd -e smbsharer

…and you’re done! From Windows, map a network drive to \\MEDIASHARE\MEDIA and enter the user smbsharer and the password you set, and you are connected.

Leave a Reply

Your email address will not be published. Required fields are marked *

Teaching the Bubble Sort Algorithm using Pringles

Creative Commons Simon Rundell CC NC-BY-SA 2025. Use freely. Or make your own. I just did it on my phone.

Leave a Reply

Your email address will not be published. Required fields are marked *

RIP Tom Lehrer

Aged 97. He gave me so much fun.

Leave a Reply

Your email address will not be published. Required fields are marked *

A network layer model helps break down communication into smaller, more manageable functions. The most common model is the OSI (Open Systems Interconnection) Model, which has seven layers, each responsible for a specific role in data transmission.

  1. Application Layer – User-facing services (e.g., web browsing, email).
    • Protocols: HTTP, HTTPS, FTP, SMTP, POP3
  2. Presentation Layer – Formats and encrypts data for applications.
    • Protocols: SSL/TLS
  3. Session Layer – Manages connections and sessions between devices.
    • Protocols: NetBIOS, RPC
  4. Transport Layer – Ensures reliable data delivery (e.g., error checking).
    • Protocols: TCP (reliable), UDP (fast but unreliable)
  5. Network Layer – Routes data between networks using IP addresses.
    • Protocols: IP, ICMP, ARP
  6. Data Link Layer – Manages data transfer between directly connected devices.
    • Protocols: Ethernet, Wi-Fi (802.11), MAC addresses
  7. Physical Layer – Transmits raw bits over physical media (cables, radio waves).
    • Examples: Ethernet cables, Fiber optics, Wireless signals

TCP/IP Model (Simplified Version)

The TCP/IP Model is a simpler version of OSI with four layers:

  1. Application Layer – Combines OSI’s top three layers (e.g., HTTP, HTTPS, FTP).
  2. Transport Layer – Manages connections (e.g., TCP, UDP). TCP has error checking, UDP does not. Data is split into packets (datagrams) which includes
  3. Internet Layer – Routes data (e.g., IP, ICMP). ICMP is Internet Connection Management Protocol and includes traceroute and ping.
  4. Network Access Layer – Physical and data link functions (e.g., Ethernet, Wi-Fi). How the packet is managed when received at rhe Lan of its destination. Packets are turned into frames and the switch forwards the right frames to the right IP address. Switch knows which MAC address has requested data and returns the data to the right one. The frame

Network Protocols

Protocols are rules and standards that define how data is transmitted across networks.

  • HTTP/HTTPS – Used for accessing websites.
  • FTP – Transfers files between computers.
  • SMTP/POP3/IMAP – Email communication. (Also X500)
  • TCP – Ensures reliable data transmission.
  • UDP – Fast, but no error checking.
  • IP – Assigns addresses to devices.
  • Ethernet/Wi-Fi – Used for local network communication.

Real-World Analogy for Network Layers – The Royal Mail Postal System

Think of network communication like sending a letter through a postal service. Each network layer has a role similar to steps in delivering a letter.


1. Physical Layer (Sending the Letter)

  • Represents the physical means of communication (wires, Wi-Fi signals, fibre optics).
  • In our analogy, this is like the postman’s satchel, postal vans, roads, and planes that physically transport your letter.

🔹 Example: Your letter is put into a satchel and carried by a van to the sorting office.


2. Data Link Layer (Stamping & Addressing)

  • Ensures that data moves correctly between directly connected devices.
  • Like adding a stamp and return address, ensuring your letter is accepted by the postal system.
  • Also checks for errors in transmission.

🔹 Example: If the letter is missing a stamp, it gets rejected.


3. Network Layer (Routing the Letter)

  • Determines the best route for data using an IP address.
  • Like a postal sorting office deciding which city or country to send the letter to.

🔹 Example: Your letter is sent from London to New York based on the address.


4. Transport Layer (Ensuring Delivery)

  • Manages the reliability of communication (e.g., TCP ensures delivery, UDP sends quickly but without confirmation).
  • Like choosing regular mail (UDP – fast but no tracking) or tracked mail (TCP – slower but reliable).

🔹 Example: If using TCP (tracked mail), the sender gets a notification when the letter arrives.


5. Session Layer (Managing the Conversation)

  • Establishes and maintains a session between devices.
  • Like a customer service call that stays connected while you get information.

🔹 Example: You keep sending letters back and forth in an ongoing conversation.


6. Presentation Layer (Translating the Message)

  • Converts data into a usable format (e.g., encryption, compression).
  • Like translating a letter from English to French if the recipient speaks a different language.

🔹 Example: If the message is encrypted (TLS), only the intended recipient can read it.


7. Application Layer (Receiving & Reading the Letter)

  • Directly interacts with the user (e.g., websites, emails, file transfers).
  • Like the recipient opening and reading the letter.

🔹 Example: The recipient reads a web page (HTTP) or opens an email (SMTP/IMAP).


Summary of the Postal System Analogy

OSI LayerPostal System Equivalent
ApplicationReading the letter (Web, Email)
PresentationTranslating the letter (Encryption, Formatting)
SessionKeeping communication open (Back-and-forth letters)
TransportEnsuring delivery (Regular vs. Tracked Mail)
NetworkChoosing the best route (Sorting Office)
Data LinkAddressing & Stamping (MAC Address, Error Checking)
PhysicalDelivering the letter (Postman’s Satchel, Roads, Aeroplanes)

Leave a Reply

Your email address will not be published. Required fields are marked *

Tablet-based tracker for in-Classroom use.

Tracking pupils mid lesson is hard. I am sure that as I become more accomplished as a teacher I can keep all this stuff in my memory, but I like objective records and I like React Apps. This therefore is a solution, orginally designed to support my pre-cataract surgery eyesight deficit, which gives me a list of students to call, to award credits or decredits to, to record any disciplinary actions or warnings which I might have to give and to record notes for each student. It also has flags to warn me of students who might need some kind of additional support, so I can check their confidential record and getn it right. It is designed to work on a tablet (such as my Surface Pro or an iPad) in landscape mode.

Here are some screenshots:

For confidentiality reasons, all data is protected behind a password system and data is transmitted between the server and client using a secure transport layer.

The user only has access to thir own data.

The main timetable page. This is test data, so don’t think I only work half the week! On each page there is a real-time clock, mainly because so many classrooms I am in have broken clocks and I want to just be able to glance at my tablet and see the time quickly.

This is the main in-class screen. It shows on the left the list of students to call. When they are called, and I record that with the “Call” button, they are moved to the bottom of the list. The red number indicates the number of times they have been called that lesson.

The + and – buttons add or subtract credits, and if I have a disciplinary issue, I can record the status of that by clicking on the discipline status. I can make any additional notes for each lesson and each one is timestamped and added to my student record.

Unfortunately in this scenario, I have had to give a minor warning to Velma to stop looking at Tik Tok in the lesson and so I have marked the first disciplinary stage and made a note in her record.

At the end of the lesson, the number of times called, the number of credits awarded and the disciplinary status at the end of the lesson is recorded in the student notes.

This gives me a record of cumultive achievement and will contribute to the reports on the students that I write.

For each class I can allocate a couple of images of the classroom layout for my seating plan. This is better and bigger than the ClassCharts one which is almost illegible.

(Obviously, I was just using any image I had to hand for testing)

In this Edit Class screen I can add students using a combination of search and a dropdown

When managing student records I can see a summary of all their notes (again useful for report-writing)

Given that a College might have a thousand students, the search facility is useful.

This project is uploaded to github at: https://github.com/SimonRundell/ClassAssistant

If anyone would like to try it out, then please let me know. I can provide the facility for a very very low price (I’m talking a tenner a year) or you can compile and implement it at your school for free. I’d be available to consult if you needed any help, support or advice on that.

Leave a Reply

Your email address will not be published. Required fields are marked *

This is the lesson plan which got me the job. I couldn’t be sure of the resources I would have available, so I opted to teach about the CIA Model (set by the College) without anything more than a PowerPoint and a bunch of envelopes and cards. Use freely. Expand as you wish. Post improvements in the comments.

[doc id=190]

The CIA Triad

Triad means 3, so there are 3 parts to the CIA triad model

Confidentiality, Integrity and Availability.

Let me illustrate with a bunch of envelopes…

Confidentiality:

Access Control

I have a list of people who are allowed the data. I ask a requesting student for their name and password which I check on a clipboard and if they are correct I give them the envelope.

I give another student the wrong password card and they ask 3 times which I reject and throw them out, showing that brute force password attacks can be foiled.

Authentication

Another student has a clipboard and a piece of paper with “Permission” on it. The requesting student gives their name and password to the clipboard student who gives me the “Permission” paper and I give the requesting student the envelope.

One issue can be the Man in the Middle attack with these two, so a student stands in between us and takes the envelope. He might not pass it on if he is a bad router and open it himself, so we must ensure the data is safe if it falls into the wrong hands with encryption.

Encryption

I have another envelope which I show as having something in code inside it. A student who is unauthorised asks for the envelope and I give them the one without the solution inside it. It doesn’t make sense. An authorised requesting student asks me for the envelope, and I give the one with the decrypted side to them, which they open and turn over which has the message on it decrypted.

Integrity:

Checksums

The requesting student asks for the envelope, I give them an envelope and tell them that the checksum of this data should be 1024, but on the outside of this envelope the checksum says “63”. I ask them if the checksum is correct and they will say no, so I give them an envelope with “1024” on it and ask if it is correct. If it is, they can open the envelope and get the data confident that it is correct.

Malware such as viruses and trojans often affect the checksum of a file, so the discarded envelopes could contain infected files.

Digital Signatures

I give the requesting student an envelope when they ask for it which I have signed. The first envelope isn’t my name. I ask them if it is correct and they say no, so they should discard it as it isn’t properly signed. I sign another one with my own signature and they can confirm it is correct and again can open it in the knowledge that it is correct.

Availability:

Backups

I create two envelopes. A student is given an envelope, and another student is encouraged to take it off them and throw it away. That is a cyber-attack. I ask him for the envelope, but it is lost. I have a second envelope somewhere, but I have to search for a moment before I hand it to them. This is restoring from a backup.

Redundancy

I create two envelopes. A student is given an envelope and then is told to throw it away. I ask him for the envelope, but it is lost. Luckily, I have a second envelope, and I give it to them immediately. This is redundancy.

These two deal with disaster recovery after systems failure, malicious damage or user error.

Denial of Service Prevention

The requesting student asks for the envelope, but all the other students start asking for the envelope at the same time, and I don’t know who to give it to. I ask the requesting student for a token, and they give me the card which says “Permission” on it and so I can give it to them, explaining that without the right token, a firewall or rate limiter will ignore all the other requests and only give the data to the right person.

Worksheet

Resources

Envelopes:

  • Basic x 5
  • Encrypted x 1
  • Encrypted with solution x 1
  • Checksum 63
  • Checksum 3794
  • Checksum 1024
  • Envelope for wrong signature
  • Envelope for right signature   = 12 envelopes in total.

Cards:

  • Password card
  • Wrong password card
  • Permission
  • Example of my signature   = 4 cards

Leave a Reply

Your email address will not be published. Required fields are marked *

Many schools often ban Mobile Phones as a distraction, but in Computing we view them as tools – both a platform for development and a means of using technology to enhance our learning. In the more relaxed environment of Further Education (16+ in the UK), we do not automatically ban Mobiles, and I want to take it further to embrace them as a tool for engagement in the Classroom, Computer Lab and Lecture Theatre.

There are tools out there but they are almost exclusively web based and this doesn’t always chime well with the workflow as the teacher has to Alt-Tab to a web page to garner student reactions and answers. This has been my main issue with Mentimeter – a great product but with a cost and an interface limited by the web browser. I wanted something which worked within Microsoft PowerPoint, so I wrote Kallbak.

www.kallbak.com

Kallback is a low cost solution for Educators to have integrated interactions with the Students in their Classroom or Lecture Theatre. With the aid of a QR code or a 6-letter shortcode, students can answer an open question, a multiple choice question, create a word cloud or respond to an image. This is displayed in the Powerpoint Presentation Slide in real time.

Whenever someone like my Dance Teacher says “Does any one have any questions?” I always ask “What is the capital of Mongolia?” just shortly before she hits me.

What’s more, you can then ask Google Gemini AI to summarise and correct the answers given.

The Presenter/Teacher/Lecturer has his own control panel to enable and disable submissions to the page shown over his presentation screen.

…thus enabling them to Start the Presentation and run multiple slides with Kallbaks on them.

The whole add-in runs separately from your Slideshow template, so you can customise the look and feel of the displayed page to match your own needs.

The kallbak.com website enables you to create your own questions, use other submitted questions and create QR codes and the necessary links to be able to take Kallbaks from your classes.

Leave a Reply

Your email address will not be published. Required fields are marked *

Image Bank

I’ve been having fun with AI image generation to support my recent revision lessons. All are available for free under the Creative Commons Licence: CC NC-BY-SA. If you are unsure of what this means please read the definition here.

Right-Click and select “Save As…”

No content yet.

The following images are drawn from other sources, and I am not necessarily sure of their copyright status. However, if you use them in an educational setting, I am sure you will be fine. Unlike those above, I do not assert any control over the images below.

This cartoon was the illustration for the original article which proposed Moore's Law

These images are all memes from t’internet. I use them to spice up slides, give encouragement or make a humorous point.

Leave a Reply

Your email address will not be published. Required fields are marked *

There are times when you need to upload a selected batch of files somewhere. In my case, as I am shipping React code to live after it has been built. Here is a .ps1 script which is for Windows PowerShell and can be run from the command line to quickly and painlessly upload a specific bunch of files to a specific upload directory.

You will need to install WinSCP but it’s free. Obviously you’ll need to change the location of the server, username, password, local file path, remove file path, files to exclude (in my case the local .config.json files) and the log file path, but after that you are ready to go! Enjoy!

Leave a Reply

Your email address will not be published. Required fields are marked *

Each year I produce a liturgical calendar of Bible readings based upon Common Worship and the Roman Lectionary which is compatible with Google Calendar, Microsoft Outlook and Mac iCal. This means it will sync to your phone and you will have a daily non-busy appointment at the top of each day with all the readings, many of the Collects and Prayers and a host of other liturgical stuff.

It is FREE, because I use it myself. If you do use it, all I ask is for your prayers: call it PrayerWare if you like.

Tell others… Share freely. Orate pro nobis.

Leave a Reply

Your email address will not be published. Required fields are marked *

However, this task has rekindled my interest as the basic engine was not that hard to write, especially given some of the simpler parameters I set myself.

  • The map would be based upon a -5 to +5 (x,y) grid, starting in the middle at (0,0).
  • ‘Treasure’ would be scattered randomly around the map.
  • The object of the game would be to pick up the items (some of which were harmful) and be entertained by my witty banter along the way
  • There was to be no violence.

The majority of the code was first drafted with the fantasy genre in mind, but later I thought ‘As this is for students, why not make it more relevant?’ So I made it about drinking.

You wander around this party picking up discarded drinks and downing them. Some of them are non-alcoholic so they have a negative score. That’s it.

The Coding

The largest section of the code is the data. These are held in Python arrays and dictionaries. The most obscure of these maps the (x,y) coordinates to a room number, and especially allocates room 0, the start in the middle. Other arrays give the text of the game. Frankly, you only need to change the descriptions, names of the rooms and the treasure descriptions and you can have any game you want.

The functions were surprisingly easy to code, especially when ChatGPT was able to correct some of my typos. To look at them, it’s probably best to walk through the main game loop.

############## MAIN GAME LOOP ##################
titles()
while True:
print(f"#####################################################")
print(f"\nThis is move {current_move}. You are currently at {room()}\n")
print(generate_random_description())
# print(f"You are carrying {list_inventory()}")
visible_items_text = items_visible()
if visible_items_text != "...nothing much, really.":
print(f"There is:\n{visible_items_text}\n") # Print only if there are items
action = parse_command(input("Enter command: ").lower())
print(action)
print(generate_random_comment())

The function room() returns the name of the current location, during movement we check to ensure the player can’t exceed the -5 to +5 border, but there is an extra check here.

generate_random_description() serves no gameplay purpose other than to add some colour with slightly quirky descriptions. There is an array of room descriptions and a random number is chosen. 25% of the time, the number will be one of the lines of text. That is why they are a little vague and nothing here has any effect on the game.

items_visible() looks to the dictionary inventory_items. It determines if that room location contains any treasure and lists them . If there isn’t any items in the room, then nothing is displayed.

The key function

Source Code

This is the source code, which can also be seen and downloaded (and played) from here

# <--- This button runs the game!

######################################################
# ##### # ##### ####### # # #
# # # # # # # # # # #
# # # # # # # # # #
# #### ####### #### # # #
# # # # # # # # #
# # # # # # # # #
# #
# A Student Quest from a long past age when #
# we still used to get free tuition. #
# #
# Coded by Simon Rundell (spr206@exeter.ac.uk) #
######################################################

import random # as in life, the only library I need.

####################### DEFINE GLOBAL VARIABLES #################################
current_location = 0 # starting position
current_move = 1 # count number of moves

inventory = [] # items you currently hold

################### DEFINE DATA STRUCTURES - LOOK AWAY NOW! #####################
# coord x, y, room_ref - +5 to -5 = 121 locations
directions = [
[-5, -5, 60], [-5, -4, 1], [-5, -3, 2], [-5, -2, 3], [-5, -1, 4], [-5, 0, 5], [-5, 1, 6], [-5, 2, 7], [-5, 3, 8], [-5, 4, 9], [-5, 5, 10],
[-4, -5, 11], [-4, -4, 12], [-4, -3, 13], [-4, -2, 14], [-4, -1, 15], [-4, 0, 16], [-4, 1, 17], [-4, 2, 18], [-4, 3, 19], [-4, 4, 20], [-4, 5, 21],
[-3, -5, 22], [-3, -4, 23], [-3, -3, 24], [-3, -2, 25], [-3, -1, 26], [-3, 0, 27], [-3, 1, 28], [-3, 2, 29], [-3, 3, 30], [-3, 4, 31], [-3, 5, 32],
[-2, -5, 33], [-2, -4, 34], [-2, -3, 35], [-2, -2, 36], [-2, -1, 37], [-2, 0, 38], [-2, 1, 39], [-2, 2, 40], [-2, 3, 41], [-2, 4, 42], [-2, 5, 43],
[-1, -5, 44], [-1, -4, 45], [-1, -3, 46], [-1, -2, 47], [-1, -1, 48], [-1, 0, 49], [-1, 1, 50], [-1, 2, 51], [-1, 3, 52], [-1, 4, 53], [-1, 5, 54],
[0, -5, 55], [0, -4, 56], [0, -3, 57], [0, -2, 58], [0, -1, 59], [0, 0, 0], [0, 1, 61], [0, 2, 62], [0, 3, 63], [0, 4, 64], [0, 5, 65],
[1, -5, 66], [1, -4, 67], [1, -3, 68], [1, -2, 69], [1, -1, 70], [1, 0, 71], [1, 1, 72], [1, 2, 73], [1, 3, 74], [1, 4, 75], [1, 5, 76],
[2, -5, 77], [2, -4, 78], [2, -3, 79], [2, -2, 80], [2, -1, 81], [2, 0, 82], [2, 1, 83], [2, 2, 84], [2, 3, 85], [2, 4, 86], [2, 5, 87],
[3, -5, 88], [3, -4, 89], [3, -3, 90], [3, -2, 91], [3, -1, 92], [3, 0, 93], [3, 1, 94], [3, 2, 95], [3, 3, 96], [3, 4, 97], [3, 5, 98],
[4, -5, 99], [4, -4, 100], [4, -3, 101], [4, -2, 102], [4, -1, 103], [4, 0, 104], [4, 1, 105], [4, 2, 106], [4, 3, 107], [4, 4, 108], [4, 5, 109],
[5, -5, 110], [5, -4, 111], [5, -3, 112], [5, -2, 113], [5, -1, 114], [5, 0, 115], [5, 1, 116], [5, 2, 117], [5, 3, 118], [5, 4, 119], [5, 5, 120]
]

# define room names - all based around a terrible house I used to share in North London when I was a student nurse.
room_names = [
"the beginning of your quest", # Center of the living room
"the living room sofa", "the coffee table", "the bookshelf", "the television stand", "the armchair",
"the door to the hallway", "the dining table", "the floor lamp", "the cluttered side table", "the rug corner",
"the kitchen doorway", "the fridge", "the kitchen counter", "the pantry", "the sink",
"the kitchen cabinets", "the stove", "the dishwasher", "the kitchen island", "the spice rack",
"the back door", "the garden path", "the flower beds", "the compost bin", "the garden shed",
"the fence corner", "the bird feeder", "the old tree stump", "the lawn chairs", "the vegetable patch",
"the garden hose", "the barbecue", "the outdoor table", "the lawn edge", "the garden gate",
"the hallway start", "the coat rack", "the shoe pile", "the umbrella stand", "the wall mirror",
"the stairs", "the closet", "the hallway table", "the front door", "the door to the street",
"the mailbox", "the driveway", "the parked bikes", "the sidewalk", "the nearby street lamp",
"the neighbor's gate", "the street corner", "the recycling bins", "the alleyway", "the hedge row",
"the old lamppost", "the bus stop", "the parking meter", "the cafe entrance", "the coffee shop interior",
"the study desk", "the small bookshelf", "the crowded bulletin board", "the study lamp", "the student sofa",
"the bean bag chair", "the coffee spill stain", "the laundry pile", "the laptop desk", "the whiteboard",
"the storage closet", "the wall clock", "the campus map", "the pile of papers", "the snacks cupboard",
"the study lounge", "the stairwell", "the elevator", "the basement lounge", "the vending machine",
"the dorm hallway", "the dorm kitchen", "the dorm bathroom", "the shower stalls", "the towel rack",
"the corridor exit", "the stairwell corner", "the lecture hall", "the lecture podium", "the professor's desk",
"the lab entrance", "the computer lab", "the row of desks", "the trash can", "the microwave",
"the vending machine corner", "the water fountain", "the campus quad", "the picnic bench", "the main entrance",
"the student union", "the cafeteria line", "the dorm common area", "the old vending machine", "the study nook",
"the main library", "the library shelves", "the library lounge", "the quiet study area", "the computer station",
"the photocopy area", "the library front desk", "the library staircase", "the library entrance", "the study garden",
"the parking lot entrance", "the bike rack", "the recycling bin", "the lawn near the quad", "the campus bench",
"the main lecture hall", "the science building entrance", "the engineering lab", "the student lounge", "the hallway bench"
]

# room descriptions are not tied to any particular room but add a little colour to the quest
room_descriptions = [
"You think you spot your girlfriend, but it turns out to be someone else.", "You think you spot your boyfriend, but it turns out to be someone else.",
"There is a pile of coats lying on the floor, but as it contains no booze, you quickly lose interest in it.", "There are completely empty glasses discarded everywhere. The place is such a mess",
"There is a noticeboard with an exam timetable on the wall. As expected, someone has drawn a cock and balls descretely in the corner.",
"There is a pile of comatose students sleeping here. There must be Student Nurses at the party as they are all turned over into the recovery position.",
"Underneath all the grime, you are disgusted to find... more grime", "An abandoned sock lies in the middle of the floor, its owner nowhere to be seen.",
"The air is thick with the smell of stale beer and cheap perfume. Just like in the song lyric which you start humming to yourself.",
"Someone has set up a makeshift DJ booth, but it’s just a phone plugged into some old speakers.", "A questionable pizza slice sits on the floor, half-eaten and very, very cold. Don't touch it.",
"A single, sad disco ball hangs from the ceiling, rotating slowly.", "A pile of shoes is here, ranging from sneakers to a single stray flip-flop.",
"A half-inflated balloon drifts aimlessly across the floor.", "The smell of stale chips fills the air. Someone spilled a whole bag on the floor and left it there.",
"You spot a half-full bottle of suspicious-looking punch. It’s probably a mix of everything left in the fridge.", "The only clean spot in the room is under an overturned chair. Everywhere else is sticky.",
"A broken chair lies in the corner, evidence of an overly enthusiastic dance-off.", "The floor is sticky with what you hope is spilled drink.",
"A lone phone charger dangles from an outlet, forgotten by its owner.", "A suspicious stain on the carpet catches your eye. You decide it's best not to investigate further.",
"Someone has attempted to start a game of beer pong, but only one cup remains standing and it is disappointly empty.",
"A plastic Halloween skeleton hangs from the ceiling, out of place and out of season.", "A single sandal sits alone on the floor, its pair likely long gone.",
"A well-meaning sign on the wall says, 'Please Clean Up After Yourself.' Clearly, no one read it.",
"A potted plant sits in the corner, wilted and forgotten, a silent witness to the chaos.", "An old pizza box doubles as a table centerpiece, though its contents are best left unmentioned and unfilched.",
"Someone has drawn a smiley face on the wall with what you hope is ketchup.", "A coat rack sags under the weight of far too many jackets. It's a miracle it hasn’t fallen over.",
"Someone has left a polite note on the fridge: 'Do Not Touch.' It’s been ignored entirely.",
"You hear faint snores from a makeshift bed made of sofa cushions on the floor.", "A lone party hat sits on the floor, slightly crumpled but still festive.",
"The remains of a birthday cake sit on the counter, now just a mess of crumbs and frosting.", "The trash bin is overflowing, and someone has started a second pile next to it.",
"A game of Jenga lies in ruins on the coffee table. It didn’t survive the night.", "An empty bottle of wine balances precariously on the edge of a windowsill.",
"A pair of sunglasses sits on the windowsill, though it’s the middle of the night.", "Someone has written 'Good Vibes Only' on the wall with a Sharpie. That'll be the deposit gone, then.",
"A laundry basket filled with unmatched socks sits by the couch. Party mystery?", "A stack of takeaway menus sits nearby, all featuring the same pizza place.",
"A flyer for a band you’ve never heard of is taped to the wall, promising 'the gig of the year.' It was last month.", "A mysterious sticky note on the fridge reads, 'Don't forget the cat!'...but no cat is in sight.",
"A plastic cup pyramid towers on the kitchen counter, each cup stained a different color.", "Someone attempted to build a pillow fort, but it collapsed long ago.",
"A pair of socks, clearly not matching, are on the floor.", "A discarded lanyard with a faded student ID dangles from the doorknob.",
"There is a faint smell of burnt popcorn, though there’s no popcorn in sight.", "An old, crumpled flyer for a 'housewarming party' sits in the corner. The date reads three years ago.",
"A single high heel lies abandoned. Cinderella story, or lost to the chaos?", "An empty mug is labeled 'Not Coffee.' Who knows what it contained.",
"A nearby table bears mysterious marks, as if someone tried to use it as a drum kit.", "A large, red bean bag chair sags under the weight of its own existence.",
"A stack of red solo cups is neatly piled on the table, ready for the next round.", "Someone has attempted to start a game of beer pong, but only one cup remains standing. And it is disappointly empty.",
]

# Define 75 items of treasure with descriptions and values. Dictionary proved to be far more efficient than any other data structure.
treasure_dict = [
{"name": "a sip of wine", "description": "A half glass of red wine with lipstick around the edge", "value": 50},
{"name": "a can that probably contained beer", "description": "A dented can of lager, still slightly chilled", "value": 30},
{"name": "a cocktail of some kind", "description": "A quarter glass of a neon-colored cocktail with a straw", "value": 15},
{"name": "a cheeky champagne flute", "description": "A flute of champagne, mostly flat but still drinkable", "value": 20},
{"name": "a wee whiskey", "description": "A strong-smelling glass of whiskey with melting ice", "value": 40},
{"name": "some punch", "description": "A plastic cup with mystery punch, sticky to the touch", "value": 25},
{"name": "coffee", "description": "A cold coffee cup with a shot of espresso left", "value": -20},
{"name": "a mug of cider", "description": "A mug three-quarters full of warm, flat cider", "value": 30},
{"name": "the remains of a vodka shot", "description": "A half-empty vodka shot glass with a lemon wedge", "value": 15},
{"name": "an energy drink", "description": "An open can of energy drink, half remaining", "value": -30},
{"name": "a can of pop", "description": "A half-finished can of fizzy pop with no fizz", "value": 10},
{"name": "a bit of a bottle of rosé", "description": "A mostly empty bottle of rosé wine", "value": 35},
{"name": "a tequila shot", "description": "A tequila shot glass with salt on the rim", "value": 15},
{"name": "a slug from a rum bottle", "description": "A quarter-full bottle of dark rum", "value": 50},
{"name": "a glug from a gin bottle", "description": "A gin bottle with a slosh left", "value": 45},
{"name": "a half pint of ale", "description": "A half-full pint glass of ale, mostly flat", "value": 20},
{"name": "a swig from an almost empty champagne bottle", "description": "An empty champagne bottle with a few drops left", "value": 5},
{"name": "someone's discarded mojito", "description": "A glass with a few mint leaves and lime, and some melted ice", "value": 10},
{"name": "a mouthful of water, just to hydrate of course", "description": "A half-empty plastic water bottle", "value": -10},
{"name": "most of a bottle of beer", "description": "An almost full bottle of beer, slightly warm", "value": 35},
{"name": "some sparkling water", "description": "A sparkling water bottle with some bubbles left", "value": -10},
{"name": "a not-so-nice cup of tea", "description": "A nice cup of tea. Stone cold.", "value": 1},
{"name": "some spiked lemonade", "description": "A solo cup of lemonade with a strong smell of vodka", "value": 30},
{"name": "a slightly lumpy glass of milk", "description": "A glass of milk left out too long, slightly warm and frankly a bit lumpy", "value": -15},
{"name": "a sour cocktail someone didn't like", "description": "A cocktail with a few sips left and a strong citrus smell", "value": 15},
{"name": "half a bottle of cola", "description": "An open cola bottle, half-full and flat", "value": -10},
{"name": "a small shot of absinthe", "description": "A small shot glass with some green liquid in it. Strange smell.", "value": 30},
{"name": "some nondescript beer", "description": "A plastic cup with two fingers of beer left", "value": 10},
{"name": "a forgotten martini", "description": "A martini glass with a single olive and a sip left", "value": 15},
{"name": "a quarter of a pint of stout", "description": "A quarter of a pint of rich stout in a glass", "value": 12},
{"name": "something out of one of those cups you see in American College films", "description": "A solo cup with a splash of something fruity and sticky", "value": -8},
{"name": "a bottle of tonic water but sadly no gin", "description": "A mostly flat bottle of tonic water", "value": -6},
{"name": "a sip from an apple juice box", "description": "A juice box with a bit of apple juice left", "value": -8},
{"name": "some stale lemonade", "description": "A glass of lemonade, missing fizz and appeal", "value": -7},
{"name": "a mouthful of red wine", "description": "A red wine bottle with a mouthful remaining", "value": 12},
{"name": "a glass of iced water", "description": "A glass of water with half-melted ice", "value": -5},
{"name": "most of a pint of lager", "description": "A pint glass three-quarters full of fizzy lager", "value": 30},
{"name": "a bourbon on the rocks", "description": "A rocks glass with - yes! - some bourbon left", "value": 28},
{"name": "a gin and tonic", "description": "A lowball glass with gin and tonic and a lime wedge", "value": 32},
{"name": "some mimosa", "description": "A champagne flute with a splash of mimosa", "value": 15},
{"name": "a rum and cola", "description": "A cola bottle with a dash of rum", "value": 25},
{"name": "the dregs of an empty beer can", "description": "An empty can of beer with a few drops left", "value": 5},
{"name": "some hot chocolate", "description": "A mug of cold hot chocolate with whipped cream residue", "value": -8},
{"name": "a strawberry daiquiri", "description": "A plastic cup with a bit of melted strawberry daiquiri", "value": 15},
{"name": "a vodka and orange", "description": "A highball glass with a splash of vodka and orange", "value": 20},
{"name": "a glass of rosé", "description": "A wine glass with some rosé at the bottom", "value": 15},
{"name": "a cup of iced coffee", "description": "A plastic cup of iced coffee with melted ice", "value": -12},
{"name": "a swallow of red wine", "description": "A wine glass with a hint of red left", "value": 5},
{"name": "a slushy", "description": "A plastic cup with melted slushy and a hint of blue raspberry", "value": -10},
{"name": "a cranberry vodka", "description": "A highball glass with cranberry vodka dregs", "value": 15},
{"name": "a lemon water", "description": "A glass of water with a lemon wedge", "value": -16},
{"name": "an iced tea", "description": "A half-empty glass of sweet iced tea", "value": -15},
{"name": "a margarita", "description": "A margarita glass with a salted rim and a sip left", "value": 15},
{"name": "a bottle of hard seltzer", "description": "A bottle of hard seltzer with a little fizz left", "value": 20},
{"name": "a half pint of lager", "description": "A half-full pint glass of flat lager", "value": 15},
{"name": "a bottle of flat cola", "description": "An open bottle of cola with some flat soda", "value": -10},
{"name": "a glass of sangria", "description": "A glass with a bit of sangria and a lonely orange slice", "value": 18},
{"name": "a glass of chardonnay", "description": "A wine glass with a last sip of chardonnay", "value": 12},
{"name": "a bitter shandy", "description": "A pint glass with a splash of bitter shandy", "value": 10},
{"name": "a glass of mulled wine", "description": "A glass of warm, spiced wine with a cinnamon stick", "value": 15},
{"name": "a pint can of cider which turned out to have a stubbed out cigarette in it", "description": "A half full pint can of cider", "value": -50},
{"name": "student's soda", "description": "A plastic cup with some mixed soda flavors", "value": 10},
{"name": "a stein of beer", "description": "A stein with a splash of flat lager", "value": 8},
{"name": "a quarter cup of punch", "description": "A red solo cup with a bit of fruit punch", "value": -7},
{"name": "a glass of milk stout", "description": "A glass with a bit of smooth milk stout", "value": 10}
]

# Assign treasures to random locations so different each game
inventory_items = []
for i, treasure in enumerate(treasure_dict): # dict equivilent of len()
location = random.randint(0, 120)
inventory_items.append({**treasure, "id": i, "location": location}) # append to end of each dictionary item

# stupid comments to amuse and inform, many of which was first used in our Text Adventure Game Flat! from 1992. ChatGPT helped with some of the others. Back then we had to read.
comment_list = [
"Edward II of England was disembowelled by a red hot poker in 1327.", "George I of England died of apoplexy after eating too many melons in 1727", "there are 775,692 words in the King James Edition of the Bible", "on the day that President Kennedy was assassinated (November 22nd 1963), Aldous Huxley died, and the Beatles were at number one with 'She Loves You'", "Alfred, Lord Tennyson composed a 6,000 word poem when he was 10",
"Frederick, Prince of Wales, was killed after being hit on the head with a cricket ball in 1751.", "Testicles, out of the scrotum look exactly like King Prawns as served up at Won Kei's Chinese Restaurant on Wardour Street. This puts you off the thought of lunch just now.", "if you could weigh all the electrons in the internet, they would total about 50 grams—roughly the weight of a strawberry.", "the blink tag in HTML was banned by the W3C (the body that manages web standards) because it was deemed too annoying for users.",
"NASA once lost a space probe to Mars because of a single typo—someone used a comma instead of a full stop, which threw the whole mission off!", "King John died after eating vast quanities of peaches and cider in 1216", "Charles VIII of France died after hitting his head on a low doorway in 1498", "The Duke of Clarence drowned in a casket of sweet wine in 1478", "the first successful dentures were patented 200 years ago by Nicholas de Chemant.",
"up to 60,000 dust mites lurk in each square yard of carpet", "there are 336 dimples on a golf ball. Are you sure you're not bored with this game already?", "James Callaghan (former Labour Party leader and Prime Minister) is the only former Chancellor of the Exchequor to have also worked for the Inland Revenue", "Fleecie Moore (no, me neither) once recorded a song entitled 'Caldonia, what makes your Big Head so Hard?'", "Prokofiev wrote the opera 'The Giant' when he was only seven", "brontophobia is the fear of thunder",
"Ron Moody was originally asked to be the third Doctor Who...", "the average Brit spends 12 years of their life watching TV", "the average man goes through 675 pairs of underpants", "Australian Cricketer Merv Hughes had his moustache insured for 2 thousand quid!", "David Bowie's father was the owner of a Soho wrestling club",
"the average snails travels at a steady 0.031 miles per hour", "you still can't remember where you were when Ronald Reagan was shot", "singer/songwriter Elvis Costello's real name is Declan Aloitius Patrick MacManus", "Ian Hendry was the original star of 'The Avengers'", "Paul Gascoigne known better as 'Gazza', originally came to fame exhorting a diet of Newcastle Brown Ale and Mars Bars.", "try as you might, you still think the opening lyrics to The Smiths 'This Charming Man' are 'An unchewed bicycle'",
"Sir Michael Hordern declined an offer to become the second Dr Who", "French punk-rocker Plastic Bertrand had a UK hit in 1978 with 'Ca Plane Pour Moi', and that you still don't know what is was about...", "it's just over hundred years since the first tins of baked beans in tomato sauce were manufactured", "Walter Hunt invented the safety pin in 1849", "US Cartoon character Mr Magoo's first name is Quincy",
"if one in every three people is Chinese...why are Bananarama all English?", "lightning travels at a speed between 100 and 1,000 miles per second", "the population of a hive of bees have to fly 50,000 miles and visit Four Million flowers to make just one pound of honey.", "The Guinness World Record for the longest hiccuping spree is 68 years.", "Cleopatra lived closer in time to the invention of the iPhone than to the construction of the Great Pyramid.", "Bananas are berries, but strawberries aren't.",
"Queen Elizabeth II was a trained mechanic during World War II.", "More people die from vending machines than shark attacks each year.", "Honey never spoils. Archaeologists have found pots of honey in ancient Egyptian tombs that are over 3,000 years old and still edible.", "Bubble wrap was originally intended to be used as wallpaper.", "Cows have best friends and get stressed when they are separated.", "In Switzerland, it’s illegal to own just one guinea pig because they get lonely.",
"When a male penguin falls in love with a female penguin, he searches the entire beach to find the perfect pebble to present to her.", "A duck’s quack doesn’t echo, and no one knows why.",
"Oxford University is older than the Aztec Empire.", "In 1999, hackers broke into NASA’s website and changed the front page to 'We’ll stop invading spaceships if you stop making them.'",
"There’s a basketball court above the Supreme Court of the United States. It's known as the 'Highest Court in the Land.'", "Vending machines kill more people each year than sharks.",
"Movie trailers were originally shown after the movie, which is why they’re called 'trailers.'", "'Party' is written in Python by the Rev Simon Rundell who is on twixxer as @frsimon"
"A group of flamingos is called a 'flamboyance.'", "Sea otters hold hands when they sleep to keep from drifting apart.", "Albert Einstein's brain was stolen after his death.",
"An octopus has three hearts.", "The inventor of the Pringles can is now buried in one.", "You cannot hum while holding your nose - and I bet you are trying it now.",
"The blob of toothpaste on a toothbrush is called a 'nurdle.'", "In France, it’s illegal to name a pig 'Napoleon.'", "There’s a McDonald's on every continent except Antarctica.", "The shortest complete sentence in the English language is 'Go.'",
"There’s a species of jellyfish that is biologically immortal.", "The shortest war in history lasted just 38 minutes, between Britain and Zanzibar in 1896.",
"Cats have fewer toes on their back paws than their front paws. Yes, you can check if you like.", "The voice of Yoda was also the voice of Miss Piggy.", "Scotland’s national animal is the unicorn.",
"A day on Venus is longer than a year on Venus.", "Tomato Ketchup was sold in the 1830s as medicine.", "In Japan, there’s a train station that has no entrance or exit, just a platform for enjoying the scenery.",
"The dot over a lowercase 'i' or 'j' is called a 'tittle.'", "Wombat poop is cube-shaped.", "High heels were originally worn by men in the 10th century.",
"A snail can sleep for three years at a time.", "The name for the shape of Pringles is called a 'hyperbolic paraboloid.' That's appropriate for a Student-Party themed game", "A single spaghetti noodle is called a 'spaghetto.'",
"Some cats are allergic to humans.", "Every continent has at least one city called Rome.", "Lobsters communicate by peeing at each other.", "Carrots used to be purple before the 17th century.",
"Your nose can remember 50,000 different scents.", "Sharks are older than trees.", "The first alarm clock could only ring at 4am which counts as cruelty to students", "Mosquitoes are the most dangerous animals in the world.",
"Bees can fly higher than Mount Everest.", "Polar bears are nearly invisible under infrared cameras.", "Butterflies taste with their feet.", "All the ants on Earth weigh about the same as all the humans."
]

############################# FUNCTIONS #########################################

def items_visible():
global current_location
global inventory_items
visible_items = [item["description"] for item in inventory_items if item["location"] == current_location]
if visible_items:
return f"{', '.join(visible_items)}."
else:
return "...nothing much, really."

#################################################################################

def list_inventory():
global inventory_items
global inventory
if inventory:
inventory_list = [inventory_items[item_id]["name"] for item_id in inventory]
inventory_value = sum(inventory_items[item_id]["value"] for item_id in inventory)
inventory_string = ", ".join(inventory_list[:-1]) + " and " + inventory_list[-1] if len(inventory_list) > 1 else inventory_list[0]
return inventory_string + f". That comes to about {inventory_value}mls worth of booze."
else:
return "Nothing. Not a sausage. Naff all. Zilch. Nada."

#################################################################################

def room():
global current_location
if 0 <= current_location < len(room_names):
return room_names[current_location]
else:
return "Unknown Location"

##################################################################################

def generate_random_comment():
# A random collection of thoughts to entertain the player
global comment_list

comment = random.randint(1, (len(comment_list) * 4)) # 1 in 4 chance of a stupid comment
if comment < len(comment_list):
response = f"\nFor some inexplicable reason you are reminded that {comment_list[comment]}\n"
else:
response = ""

return response

#################################################################################

def generate_random_description():
# A random collection of thoughts to entertain the player
global room_descriptions

room_desc = random.randint(1, (len(room_descriptions) * 4)) # 1 in 4 chance of a stupid room_desc
if room_desc < len(room_descriptions):
response = f"\n{room_descriptions[room_desc]}\n"
else:
response = ""

return response

#################################################################################

def room():
global current_location
# Ensure current_location is within bounds
if 0 <= current_location < len(room_names):
return room_names[current_location]
else:
return "Unknown Location"

#################################################################################

def get_coords():
global directions
for location in directions:
if location[2] == current_location:
return [location[0], location[1]]

#################################################################################

def set_coords(x, y):
global directions
for location in directions:
if x == location[0] and y == location[1]:
return location[2] # returns the room given by x and y

#################################################################################

def process_direction(direction):
global current_location # Declare global to modify the global variable
# Get x, y based on the current location
position = get_coords()
#print("DEBUG: position before = ", position)

# Apply boundary checks for each direction
if direction == 'north' and position[1] < 5:
position = [position[0], position[1] + 1]
elif direction == 'south' and position[1] > -5:
position = [position[0], position[1] - 1]
elif direction == 'east' and position[0] < 5:
position = [position[0] + 1, position[1]]
elif direction == 'west' and position[0] > -5:
position = [position[0] - 1, position[1]]

# print("DEBUG: position after = ", position)

# Get new location based on x, y if within bounds
new_location = set_coords(position[0], position[1])
if new_location is not None:
current_location = new_location
else:
print("You can't move further in that direction.")

# print("DEBUG: current_location = ", current_location)
#################################################################################

def check_item_here(item_name):
global inventory_items
global inventory
global current_location
item_name = item_name.lower()

for item in inventory_items:
if item["location"] == current_location and (item_name in item["name"].lower() or item_name in item["description"].lower()):
inventory.append(item["id"])
item["location"] = -1 # Mark as taken
return True # Exit the function after taking the item
return False # Return False if the item is not found


#################################################################################

def parse_command(command):

global current_move
# Convert command to lowercase for easier matching
command = command.lower().strip()

# Split command into words
words = command.split()

# prepare response with a neutral phrase in case it falls through
# the parser
response = "Come again?"

# check for help
if len(words) == 1 and words[0] == "help":
response = f"\n\nHELP: To move in a direction GO <direction> ie GO NORTH.\n"
response += f"INVENTORY will give you a list of items you have drunk.\n"
response +=f"You can TAKE, GET, or DRINK the items in your location\n\n"
return response

if len(words) == 1 and words[0] =="inventory":
drunk_synonym = random.choice(["You have so far imbibed",
"So far, you have got away with chugging",
"No one so far has seen you knock back",
"No wonder you're swaying, you've had"])

response = f"{drunk_synonym} {list_inventory()}"
return response

# Check for direction commands (e.g., "go north")
if len(words) == 2 and words[0] == "go":
direction = words[1]
if direction in ["north", "south", "east", "west"]:
process_direction(direction)
extra_response = random.choice(["", "", "", "", "", "",
"It looks a bit nicer there.",
"I reckon it's safer.", "Less sticky.",
"You leave that mess behind you."])
response = f"Moving {direction}. {extra_response}\n"
current_move += 1
return response
else:
# decided not to punish player for not getting the parser to understand
# current_move += 1
return "I have no idea what you are talking about."


elif len(words) == 2 and (words[0] == "eat" or words[0] == "nibble" or words[0] == "scoff"):
response = f"I really wouldn't risk it, if I were you.\n"
return response

# Check for "take" commands (e.g., "take axe" or now "drink beer")
elif len(words) == 2 and (words[0] == "take" or words[0] == "get" or words[0] == "drink"):
item = words[1]
if check_item_here(item):
extra_response = random.choice(["", "", "", "", "",
"Nice.", "Gulp.", "Weeee like to drink with ---",
"It tastes a bit gritty and nicotine-y.", "Bleugh.",
"That was clearly another one of life's regrets...",
"Parts of you are beginning to get refreshed.",
"It goes down quickly, even though it makes you wince a bit."
"","", "", "", ""])
response = f"\nDrinking {item}. {extra_response}\n"
current_move += 1
else:
extra_response = random.choice(["",
"Are you hallucinating?",
""])
response = f"\nThe {item} is not here. {extra_response}\n"
return response

# If the command doesn't match any known patterns
else:
# decided not to punish player for not getting the parser to understand
# current_move += 1
return "I have no idea what you are talking about."

'''
Before my genius idea to make this about my old student days
I was going to make this a general fantasy adventure, but
for the purposes of this game, these commands are not going
to be needed...

# Check for "drop" commands (e.g., "drop book")
elif len(words) == 2 and words[0] == "drop":
item = words[1]
response = f"Dropping {item}"
current_move += 1
return response

# Check for "use" commands (e.g., "use key")
elif len(words) == 2 and words[0] == "use":
item = words[1]
response = f"Using {item}"
current_move += 1
return response

# Check for "read" commands (e.g., "read book")
elif len(words) == 2 and words[0] == "read":
item = words[1]
response = f"Reading {item}"
current_move += 1
return response

# Check for "open" commands (e.g., "open box")
elif len(words) == 2 and words[0] == "open":
item = words[1]
response = f"Opening {item}"
current_move += 1
return response

# If the command doesn't match any known patterns
else:
# decided not to punish player for not getting the parser to understand
# current_move += 1
return "I have no idea what you are talking about"
'''

def titles():
response = "#####################################################\n"
response += "# ##### # ##### ####### # # #\n"
response += "# # # # # # # # # # #\n"
response += "# # # # # # # # # #\n"
response += "# #### ####### #### # # #\n"
response += "# # # # # # # # #\n"
response += "# # # # # # # # #\n"
response += "# #\n"
response += "# A Student Quest from a long past age when #\n"
response += "# we still used to get free tuition. #\n"
response += "# #\n"
response += "# Coded by Simon Rundell (spr206@exeter.ac.uk) #\n"
response += "#####################################################\n\n"
response += "Long, long ago, before being a student required sobriety,\n"
response += "parties used to be long and debauched. At the end of them\n"
response += "reprobate students short on money would hoover up any\n"
response += "left over drinks in the vain hope of maintaining the buzz.\n\n"
response += "We all know that this was irresponsible and silly and\n"
response += "should never be done, but Students these days are much more\n"
response += "responsible, so you get to relive my past by wandering\n"
response += "around this party drinking yourself silly. Beware, not all\n"
response += "that sounds like liquid is a nice drink...\n\n"
response += "For help, type HELP\n\n"
print(response)

############## MAIN GAME LOOP ##################
titles()
while True:
print(f"#####################################################")
print(f"\nThis is move {current_move}. You are currently at {room()}\n")
print(generate_random_description())
# print(f"You are carrying {list_inventory()}")
visible_items_text = items_visible()
if visible_items_text != "...nothing much, really.":
print(f"There is:\n{visible_items_text}\n") # Print only if there are items
action = parse_command(input("Enter command: ").lower())
print(action)
print(generate_random_comment())


Leave a Reply

Your email address will not be published. Required fields are marked *

This little revision aid comes out of a less ambitious idea: I wanted to illustrate the matching of statements to headings using a little drag and drop. It was originally going to be within PowerPoint, but VBA and PowerPoint is a little weak in this area: a lot of shapes simply wouldn’t be dragged around for the purposes of education or entertainment. So I decided to flip the web and write it in React. Don’t worry: if none of that made sense and you just want to know how to do the questions because you don’t teach Computing, it’s okay – here is my solution!

Simply drag and drop the answer over the question and if that is correct then it turns green, red if you are wrong. You can correct your answer by dragging the right answer onto the answer and the wrong one will reappear. You can go straight to any given question by identifying the question number (0 is the first one, I’m a Computer Scientist, of course it is zero-indexed!) and copying the url. This is good when you want to just use it as a Knowledge Retrieval demonstration with the whole class.

The data is stored in a file called config.json and is quite easy to configure:

{
  "QuestionSets": [
    {
      "Header": "Match the computer science term with its definition",
      "QuestionAnswerPairs": [
        { "Question": "Algorithm", "Answer": "A step-by-step procedure to solve a problem or perform a task." },
        { "Question": "Binary", "Answer": "The base-2 number system used by computers, consisting of 0s and 1s." },
        { "Question": "CPU", "Answer": "The central processing unit, responsible for executing instructions." },
        { "Question": "Cache", "Answer": "A small amount of high-speed memory located inside or close to the CPU." },
        { "Question": "RAM", "Answer": "Volatile memory used to temporarily store data and instructions." }
      ]
    },
    {
      "Header": "Match the computer science term with its definition (Part 2)",
      "QuestionAnswerPairs": [
        { "Question": "ROM", "Answer": "Non-volatile memory that contains essential instructions, such as the boot process." },
        { "Question": "Bit", "Answer": "The smallest unit of data in a computer." },
        { "Question": "Byte", "Answer": "A group of 8 bits, often representing a single character." },
        { "Question": "Software", "Answer": "Programs and operating information used by a computer." },
        { "Question": "Hardware", "Answer": "The physical components of a computer." }
      ]
    }
]

I’m happy to let you have the source code for you to implement it on your own server, or you can have it on my revision domain as <name>.examrevision.online where <name> is whatever you want, for only £10.00 a year. Contact me on simon@rundell.org.uk

Leave a Reply

Your email address will not be published. Required fields are marked *

This is such a cool effect, bringing an object (usually a div) up on screen

@keyframes blurEffect {
    from {
      filter: blur(16px);
    }
    to {
      filter: blur(0px); 
    }
  }

  .blur {
    animation: blurEffect 0.5s ease-in forwards;
    -webkit-animation: blurEffect 0.5s ease-in forwards;
}
<div class="blur">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. In nec justo aliquet, porta nisi quis, sollicitudin nisl. Nunc ac molestie odio. Phasellus vitae commodo leo, quis accumsan eros. Curabitur at nisi pulvinar, tempus odio vel, ullamcorper neque. Ut scelerisque semper rutrum. Etiam odio elit, malesuada eget lobortis id, efficitur id metus. Praesent suscipit efficitur faucibus.
</div>

Leave a Reply

Your email address will not be published. Required fields are marked *

I tried to install regular JupyterHub about a dozen times. Each time I found myself in a morass of configuration and authentication problems, and when it came to the crunch, I could not get Python Kernel to initialize on it. Given that this was the key point of running a Jupyter Notebooks Server, it was all a bit disappointing.

Then I discovered The Little Jupyter Hub (aka TLJH) and it simply worked. It’s configuration is a little less straightforward, but my Python teaching site: https://www.pythonlessons.io is now fully up and running.

You can find the site for it here: https://tljh.jupyter.org/en/latest/ but below I will document my process and how I learned to customise the templates to give it my own (or your organization’s) look and feel.

Pre-requisites

  1. Some familiarity with the command line.
  2. A server running Ubuntu 20.04+ where you have root or sudo access (Ubuntu 22.04 LTS recommended).
  3. At least 1GB of RAM on your server.
  4. Ability to ssh into the server & run commands from the prompt.
  5. An IP address where the server can be reached from the browsers of your target audience.

Step 1: Installing The Littlest JupyterHub

  1. Using a terminal program, SSH into your server. This should give you a prompt where you can type commands.
  2. Make sure you have python3python3-devcurl and git installed.
sudo apt install python3 python3-dev git curl

3. Copy the text below, and paste it into the terminal. Replace <admin-user-name> with the name of the first admin user for this JupyterHub. Choose any name you like (don’t forget to remove the brackets!). This admin user can log in after the JupyterHub is set up, and can configure it to their needs. Remember to add your username!

curl -L https://tljh.jupyter.org/bootstrap.py | sudo -E python3 - --admin <admin-user-name>

4. Press Enter to start the installation process. This will take 5-10 minutes, and will say Done! when the installation process is complete.

5. Copy the Public IP of your server, and try accessing http://<public-ip> from your browser. If everything went well, this should give you a JupyterHub login page.

6. Login using the admin user name you used in step 3. You can choose any password that you wish. Use a strong password & note it down somewhere, since this will be the password for the admin user account from now on.

7. Congratulations, you have a running working JupyterHub!

Step 2: Adding more users

Most administration & configuration of the JupyterHub can be done from the web UI directly. Let’s add a few users who can log in!

  1. In the File menu select the entry for the Hub Control Panel.

2. In the control panel, open the Admin link in the top left.

3. Click the Add Users button.

4. Type the names of users you want to add to this JupyterHub in the dialog box, one per line.

You can tick the Admin checkbox if you want to give admin rights to all these users too.

5. Click the Add Users button in the dialog box. Your users are now added to the JupyterHub! When they log in for the first time, they can set their password – and use it to log in again in the future.

Step 3: Adding a Secure Certificate

Where it says <your domain> add your domain name and <your email> add your own email

sudo apt update
sudo apt install certbot
sudo systemctl stop jupyterhub.service
sudo systemctl stop traefik.service
sudo certbot certonly --standalone -d <your domain> --email <your email> --agree-tos

sudo nano /opt/tljh/config/config.yaml

Add the following text to config.yaml:

Once again, substitute <your domain> for the actual domain name you are going to use.

https:
enabled: true
tls:
cert: /etc/letsencrypt/live/<your domain>/fullchain.pem
key: /etc/letsencrypt/live/<your domain>/privkey.pem

Then restart the configuration with:

sudo tljh-config reload proxy
sudo systemctl start jupyterhub

Test by trying to access the site with https://<your domain> and it should be ready to run!

Step 3: Change the look and feel

The files for TLJH are stored in  /opt/tljh  which is different to a standard JupyterHub installation, but the principles remain the same. I found that the config folder wasn’t available to me even when I tried to enter it with sudo

The solution was to take ownership of that folder (either permanently or temporarily) with

sudo chown -R <your user name>:<your user group> /opt/tljh/config

You don’t have to do this, but I like to see what’s in there…

Inside the config folder is the config.yaml we edited earlier. There is also a folder called jupyterhub_config.d which at this point is probably empty. It can contain a number of python configuration files which are processed in alphabetical order as the server operates. We need to create a file (of any name) in there. I chose templates.py

sudo nano /opt/tljh/config/jupyterhub_config.d/templates.py

In that file I put the following:

c.JupyterHub.template_paths=['/home/templates']
c.Spawner.default_url = '/lab'
c.JupyterLabApp.default_settings_overrides = {
"theme": "JupyterLab Light"
}

The first command sets a new folder for the templates so I am able to modify them. The last two commands forces the default theme of the site to be the light theme (which matched my design much better). The user can toggle between light and dark.

You now need to copy the templates to /home/templates so you can work on them. You could put it anywhere, but I found it easier to be able to access them in a directory which was shared by Samba to my PC.

mkdir /home/templates
sudo cp -R /opt/tljh/hub/share/jupyterhub/templates/* /home/templates

You can then modify the text in these templates, and by adding a <style> css section in page.html you can override the default styles.

Here is my page.html – notice that I force these css styles with !important

 <head>
<style>

#login-main .auth-form-header {
background: #cccccc !important;
}

.btn .btn-jupyter .form-control {
background: #cccccc !important;
}

body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 0;
}

.container {
max-width: 500px;
margin: 50px auto;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
border-radius: 8px;
}

.auth-form-header h1 {
text-align: center;
color: #333333;
margin-bottom: 20px;

}
.auth-form-body {
display: flex;
flex-direction: column;
gap: 15px;
}

.form-control {
padding: 10px;
border: 1px solid #cccccc;
border-radius: 4px;
font-size: 16px;
}

.btn-jupyter {
--bs-btn-bg: #cccccc !important;
--bs-btn-border-color: #000000 !important;
color: #000000 !important;
border: none;
padding: 10px;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s ease;
}

.btn-jupyter:hover {
background-color: #0056b3;
}

.login_error {
color: #ff0000;
text-align: center;
}

.login_terms {
display: flex;
align-items: center;
gap: 10px;
}

.login_terms a {
color: #007bff;
text-decoration: none;
}

.login_terms a:hover {
text-decoration: underline;
}

.feedback-container {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}

.feedback-widget {
display: flex;
align-items: center;
}

.fa-spinner {
font-size: 24px;
color: #007bff;
}
.pythontitle {
font-size: 36px;
font-weight: bold;
color: #333333;
text-align: center;
}

.borderedtext {
border: 1px solid #cccccc;
padding: 20px;
border-radius: 4px;
font-size: 16px;
text-align: center;
margin: 20px;
}

</style>
<meta charset="utf-8">

and in login.html my changes/additions look like this:

 {% else %}
<div class="pythontitle"><img src='{{ base_url }}logo' alt='JupyterHub logo' class='jpy-logo' title='pythonlessons.io' /></div>
<div class="borderedtext">FREE Secure Python Development Environment</div>
<form action="{{ authenticator_login_url | safe }}"
method="post"
role="form">
<div class="auth-form-header">
<h1>Sign in</h1>
</div>
<div class='auth-form-body m-auto'>
<p id='insecure-login-warning' class='hidden'>
Warning: JupyterHub seems to be served over an unsecured HTTP connection.
We strongly recommend enabling HTTPS for JupyterHub.
</p>
{% if login_error %}<p class="login_error">{{ login_error }}</p>{% endif %}
<input type="hidden" name="_xsrf" value="{{ xsrf }}" />
<label for="username_input">Username:</label>
<input id="username_input"
type="text"
autocapitalize="off"
autocorrect="off"
autocomplete="username"
class="form-control"
name="username"
val="{{ username }}"
autofocus="autofocus" />
<label for='password_input'>Password:</label>
<input type="password"
class="form-control"
autocomplete="current-password"
name="password"
id="password_input" />
{% if authenticator.request_otp %}
<label for='otp_input'>{{ authenticator.otp_prompt }}</label>
<input class="form-control"
autocomplete="one-time-password"
name="otp"
id="otp_input" />
{% endif %}
<div class="feedback-container">
<input id="login_submit"
type="submit"
class='btn btn-jupyter form-control'
value='Sign in'
tabindex="3" />
<div class="feedback-widget hidden">
<i class="fa fa-spinner"></i>
</div>
</div>
{% block login_terms %}
{% if login_term_url %}
<div id="login_terms" class="login_terms">
<input type="checkbox"
id="login_terms_checkbox"
name="login_terms_checkbox"
required />
{% block login_terms_text %}
{# allow overriding the text #}
By logging into the platform you accept the <a href="{{ login_term_url }}">terms and conditions</a>.
{% endblock login_terms_text %}
</div>
{% endif %}
{% endblock login_terms %}
</div>
</form>
{% endif %}
{% endblock login_container %}
<div class="borderedtext">If you have forgotten your password, please contact your teacher or lecturer.</div>
<div class="borderedtext">Academic institutions wishing to take advantage of this FREE resource can contact the administrator: <a href="mai>
</div>
{% endblock login %}

I suppose I cheated with the logo. I could have uploaded a fresh logo and changed the call to it, but instead I edited the original and stored it in the main static files: /opt/tljh/hub/share/jupyterhub/static/images. It works, so I’m not unhappy.

End Result:

Leave a Reply

Your email address will not be published. Required fields are marked *

I’ve been working on JupyterHub Notebooks recently – a collaborative document format which can run Python code in small code windows. It’s excellent for teaching and developing skills in programming.

To this end I have been working on draft 1 of an adventure where you need to solve a series of puzzles using various Python skills such as conditional statements, iteration, arrays and the like. At the moment it needs some work because I think it starts with something quite complex, and I would like to begin with puzzles which start at the basics of coding and then build up culminating in the challenge of creating a little game.

The code is:

message = ".ebordraw em ni xob eht dniF .87 eb rebmun ykcul em dna 8171 eb htrib ym fo raey ehT"
# decode the message in the line below
decoded_message = message[::-1]
print("The decoded message is", decoded_message)

That’s a bit hard to start with, so I’m working on other clues with something more basic. I also have little sections which have the answer hidden – if it’s text the answer is encoded in an md5 string and if it’s a number there is a complex calculation and these tell you if you are on the right track.

At the end, as I said, there is an opportunity to build a little game. It could go anyway, but it would give you the opportunity to see how far a student has got with his/her programming skills.

Code (this would be my solution:

import random
turns = 0
injuries = 0

for turns in range(1,10):
walk_direction=input(f"Step {turns} Current injuries {injuries}: Turn Left or Right?").lower()
print(f"You turn to the {walk_direction}")
injury = random.randint(0,1)
if injury==1:
injuries = injuries + 1
print(f"Oh, no a trap causes you an injury. You now have {injuries} injuries on your body!")
if injuries>2:
print(f"You have died from an excessive amount of injuries.")
break

if injuries<3:
print("You have survived the dangerous path through the cave of the skull!")

This is an animated GIF of it running:

I think the method could be applied to a number of genres: spy stories, crime detection and the like. I just need to get the puzzles a little more refined. It’s a good start.

Leave a Reply

Your email address will not be published. Required fields are marked *