Thursday, October 12, 2023

Creating a 4-Pointed Dice App to Play the Royal Game of Ur

Want a free copy of this project? Go to nowhereroad.com/ur for the Macintosh or Windows version.
Note: Neither app is officially certified by Apple or Microsoft, so you'll need to follow instructions on the web page for how to circumvent the security blocks.

I am again teaching the Design Thinking course this semester for doctoral students. This week is game night. I invite everyone to bring in a favorite, family friendly game to share. We then spent about 75 minutes playing games while eating pizza and snacks followed by a discussion about which games are the most fun to play and why. At some point, usually in the weeks to follow  we'll extend these ideas and experiences to games in education. But, the goal this week is just to play some familiar games and revisit the experience of enjoying games and, mostly importantly, seeing if the "play phenomenon" reveals itself.

I like to take advantage of game night to encourage people who have never played chess to consider trying it out. I set up a couple of chess boards and send people some recommendations on short videos with the basics of the rules. A lot of adults who have never played chess often consider it "too late" to learn a game like chess. Off course, this is not true. A few people usually take me up on the offer.

This year I'm also introducing everyone to possibly the oldest game for which we know the basic rules, namely the Royal Game of Ur. The games dates back at least to about 4500 years. Tom Scott, one of my favorite YouTubers, made an excellent and very entertaining video introduction to the game featuring Irving Finkel of the British Museum: 

The Wikipedia entry for the game is also a good place to learn more about it (Irving Finkel is likewise featured in the entry).

An interesting part of the game is the use of 4-pointed dice. Two of the four points are painted a different color from the other two. Consequently, each die gives you a 50% chance of getting a colored point. You add up the number of colored points facing up, to determine how many spaces you can make on your next move. The result is a number somewhere between 0 and 4. Unfortunately, I don't own any 4-pointed dice. I've been using a fairly creative hack involving a traditional pair of 6-side dice. I throw the dice and simply subtract the two numbers, resulting in an answer from 0 to 5. It's important that a result of 0 is a possibility. The highest possible number can only be four, so if you throw a 6 and a 1 resulting in 5, you simply throw the dice again. This hack works well enough, but I have a feeling that the probabilities of the possible answers are very different from the original game.

So, late on a Friday afternoon, I decide to quickly make an app with LiveCode to simulate the throwing of the 4-pointed dice. This is probably the simplest little project I've ever written about on this blog. I had a functional prototype in about 15 minutes showing a string of four numbers - either 1s or 0s - each time a button is clicked. But, I really wanted to keep the experience in the same feeling of the game itself. So, I took an extra 90 minutes or so to improve the graphic design and tweak the interface. Here's a screen shot of a roll of the dice showing a total result of 2:


I know you can't read it, but the text at the bottom provides minimal directions: "Directions: Click anywhere in the window or press the space bar for a new roll." So yes, to roll the dice, you either just click inside the app window or press the Space Bar. There is an invisible button covering the entire area of the app's screen.

As simple as this project was to produce mathematically, creating the visual illusion of the dice forced me to relearn a basic skill, namely how to "skin" a button. That is, how to apply a graphic to a button.

Dissecting the Code

Here is the code for the button titled "Throw the Dice" - this button covers the entire screen and is not visible:

1:  on mouseup  
2:      
3:    hide button "die1"  
4:    hide button "die2"  
5:    hide button "die3"  
6:    hide button "die4"  
7:      
8:    put random(2)-1 into varResult1  
9:    put random(2)-1 into varResult2  
10:    put random(2)-1 into varResult3  
11:    put random(2)-1 into varResult4  
12:      
13:    put varResult1 into field "result1"  
14:    put varResult2 into field "result2"  
15:    put varResult3 into field "result3"  
16:    put varResult4 into field "result4"  
17:      
18:    if varResult1 = 1 then set the icon of button "die1" to 1023   
19:    else  
20:     set the icon of "button die1" to 1012  
21:    end if  
22:      
23:    if varResult2 = 1 then set the icon of button "die2" to 1023   
24:    else  
25:     set the icon of "button die2" to 1012  
26:    end if  
27:      
28:    if varResult3 = 1 then set the icon of button "die3" to 1023   
29:    else  
30:     set the icon of "button die3" to 1012  
31:    end if  
32:      
33:    if varResult4 = 1 then set the icon of button "die4" to 1023   
34:    else  
35:     set the icon of "button die4" to 1012  
36:    end if  
37:      
38:    //show correct image  
39:    put varResult1 + varResult2 + varResult3 + varResult4 into varScore  
40:    put varScore into field "score"  
41:      
42:    wait .5 seconds  
43:    put .2 into varWait  
44:    show button "die1"  
45:    wait varWait seconds  
46:    show button "die2"  
47:    wait varWait seconds  
48:    show button "die3"  
49:    wait varWait seconds  
50:    show button "die4"  
51:      
52:  end mouseup  
53:    

Having the Computer Compute the Result: Not Used in the End

The non-graphical version I completed in 15 minutes actually told the user the score of the toss in addition to show the list of 1s and 0s representing the result of each die. In the end, I decided against showing this mathematical results because not only was it not necessary, I felt it interfered with the game play. Players can obviously see for themselves the result just by looking at the dice, just as they would if the dice were physical objects. But, I left that code in for the heck of it. Here are the lines of code that now don't serve any function:

Lines 13-16 reveal the numerical result of each die - either a 1 or a 0 - in four separate fields.

Line 39 adds up each of the four results and put the final answer in the variable varScore. Line 40 displays varScore in the (now invisible) field "score."

Let's now take a look at the lines of code that remain necessary for the app to work.

Throwing the Dice and Getting the Visual Result

There are four buttons that represent each of the four die titled simply die1, die2, die3, and die4. These buttons serve no other function other than displaying the correct graphic, either with a blank point or a colored point. That may seem like an odd use of a button, but it works well.

Lines 3-6 hide the four buttons.

Lines 8-11 computer a random number from 0 to 1. The random function actually doesn't start with 0, so a command such as Random(2) will return either a 1 or a 2. So, all I did was subtract one from the output to get 0 or 1. Four random numbers are computed, one for each die using the variables varResult1, varResult2, varResult3, and varResult4.

Next are four groups of code that all work the same, but for each of the four die. So, let's dissect just the first group for die1, namely lines 18-21. Each group is a classic IF/THEN/ELSE construction. If the result of "die1"

Line 18 says that if varResult1 is a 1, then set the icon of button "die1" to 1023. Line 20 says if the result is not 1 (well, it must be a ), then set the icon to 1012. This is the code to skin the button with a pre-existing graphic stored somewhere in the stack. In this case,  there is a second card that stores the following images:


There are four images on this card, but only the top two are used. The others were examples I decided not to use. All objects in LiveCode are automatically assigned an ID number upon creation. The ID for the top left graphic is 1012 (no colored point) and the top right graphic has the ID of 1023. The button and the image have exactly the same dimensions, so the button takes on that graphical "skin." 

Lines 42-50 show the final results. Line 42 pauses for a half second before the four buttons, hidden a moment ago, before moving forward with revealing the results. Yes, the goal is to provide just a taste of anticipation. Lines 44, 46, 48, and 50 show each of the buttons in sequence, but with a very small pause between each, again trying to recreate some of the fun tension of the dice being thrown with the player taking a moment to scan the results. I wasn't sure how long a pause to provide, so rather than having to update four separate lines with a different wait time, I created the variable varWait to hold the wait time. This allowed me to play around with different values to get what I thought was the right time.

Allowing the User to Press the Space Bar Instead of Clicking the Button

As I tried out the app from the perspective of someone playing the Game of Ur, I thought the experience would be enhanced by letting the user just press the Space Bar instead of pressing the mouse button. It's a simple, but much better approach to the interface design. However, this was another skill I had to relearn as well. Luckily, I just looked up one of my old LiveCode projects to see how I did it previously. This code is place on the Card's script:

1:  on keydown theKey   
2:    Global isReading   
3:    if isReading = 0 Then   
4:     if TheKey = space Then   
5:       send mouseup to button "throw the dice"   
6:     end if   
7:    End If   
8:    if TheKey <> space Then   
9:     pass keydown   
10:    End If   
11:  end keydown   

Lines 4 and 8 says that if the key being pressed is the space key, then "send mouseup" to the button "throw the dice." This is equivalent to the user doing the same thing.

Adding Just One More Thing

I was about to publish this post, but thought I should wait until after game night to do so, just in case I needed to change or fix something. Of course, one needs to always be careful of "creeping featurism." That is, continually adding features to the point that you ruin the app. All worked well on game night, but it occurred to me at one point that dice don't line up neatly in a row after you throw them. So, I thought one more feature would add to the game experience, namely to have the dice appear at random spots on the screen.

This sounds simple enough, but there are traps to avoid. For example, one must be careful with random number generators. What would happen if, by chance, the computer chose the almost exact same location for two of the die? Well, you likely wouldn't be able to see the result of one of the die. So, I had one of those interesting little math problems to solve - where to randomly place each of the four die so that it is guaranteed that all four dice would clearly show their result.

To solve this, I graphed out the problem. The app's screen size is a convenient 400X800 with the origin located in the top left corner of the app window (with apologies to René Descartes). So, I decided to keep each die in their vertical "lane" of 200 pixels. With that in mind, the lane for each die, in order, is the following:

  • 0-200
  • 200-400
  • 400-600
  • 600-800
(Yes, I'm ignoring the 1 pixel overlapping at the edges of lanes.)

Each die's dimensions are 150X150 pixels, so it is important that the center of each die (which officially marks the location of any LiveCode object) is at least 75 pixels away from the center.

So, here is the grid I sketched out on a scrap of paper: 


The fundamental algorithm turned out to be the following:

   set the location of button "die1" to random(40)+X,random(210)+75

The width of the "display lane" for each die is 50 pixels. This was derived by subtracting 75 from both the left and right of the total width of the die's lane: 200-75-75=50. So, the left edge of each display lane is X. For die1 it's 0 because it is on the far left. X for the second die is 275, (200+75). X for the third die is 475 (400+75). Finally, X for the fourth die is 675 (600+75).  

Consequently, here is the code for moving each of the die to a random location:

   set the location of button "die1" to random(50)+75,random(210)+75
   set the location of button "die2" to random(50)+275,random(210)+75
   set the location of button "die3" to random(50)+475,random(210)+75
   set the location of button "die4" to random(50)+675,random(210)+75

Yes, yes, a rather impressive command of elementary school mathematics. (Well, I was a 5th grade teacher for awhile after all.)

Here is a screenshot of one throw of the dice:


Final Thoughts

This was a fun project with an impulsive motivation to do it. As is typical, getting a prototype to work functionally takes very little time in comparison to designing the user and graphical interface. Of course, the most time spent on this project was the time it took to write this blog post. 

Oh, one last thing... I also worked out the probabilities of each of the possible results of throwing four 4-pointed dice:

  • 0: 1 in 15
  • 1: 4 in 15
  • 2: 5 in 15
  • 3: 4 in 15
  • 4: 1 in 15
I think knowing this contributes to the strategy of the game.

Monday, June 19, 2023

Lloyd's Screencast Teleprompter App



 "Video is the new PowerPoint," or so the saying goes, even though I'm a Google slides guy myself. I make a lot of videos for my teaching and most consist of me providing narration for a collection of slides. I use the "shelf life time ratio" to determine how much planning should go into each. That is, I do very little planning for a video I intend a specific audience to watch just once. For these, I'm basically "shooting from the hip" and making things up as I go. In contrast, a video with an intended long shelf life - videos I plan on using semester after semester - requires a lot of planning up front. The ratio is simply the longer the shelf life, the more planning that is required.

For long shelf life videos, I long ago learned to write scripts, at least for the very beginning of the video and key sections. Viewers will get turned off quickly if the first 30 seconds of the video is of a bumbling and stammering professor who isn't sure what he wants to say. But where to display one's scripts or notes while making the video? Sometimes I taped the script to the computer screen. Yet, I've made too many videos where it is painfully obvious that I'm reading something. You can see my eyes moving back and forth and you can slowly see my gaze go from top to bottom. I've tried posting small script portions on a stand or box sitting just behind my computer as close to the camera as possible. These results are better, but I'm forced to have a bunch of cuts as I trade out each new portion. I'd much prefer to be known as "one take Rieber." I've been struggling with this issue for almost 20 years. So, it kind of shocks me that it took me this long to have the idea to build my own teleprompter for screencasts.

For the record, I have tried using third party teleprompters in the past for videos where I'm being filmed without a computer. I've never been too comfortable with them. It's hard to get the speed right and it's a challenge of where to put them in relation to the camera. (You must realize that I have no budget and I am my own "video crew," so I'm constantly jerry-rigging up the equipment.) But, at least 97% of the videos I make are screencasts - me narrating a set of slides or some other application, with me in a small video window off to the side. What I really need is a small, compact teleprompter that can be displayed on the screen near the computer's camera with plenty of screen space remaining for the window I wish to film. It seemed like a perfect LiveCode project.

My Third Design Works Like a Charm

I'm very happy with the design of the app, but I had two painful failures at the start. I'll get to those later, but allow me to show you the current design. I'm still working on the graphic design, but my field tests of the functionality have gone very well. Here is a short video demonstration:

Here are some of the main design principles of the app:

  • Controlling the speed of the teleprompter needs to be easy and unobtrusive, so that the user could change the speed while narrating with virtually all attention devoted to the meaning of the text.
  • The teleprompter needs to consume very little screen space.
  • The teleprompter needs to be able to be positioned as close to the computer camera as possible so that it appears that the user is looking at the viewer.
  • The text size needs to be under the user's control.
  • The size of the teleprompter window needs to be under the user's control.
  • The text can be pasted into the teleprompter and then edited during the rehearsal of the narration.

Of these, the first is the most important. I needed a way to let the user alter the speed of the teleprompter easily throughout the narration, stopping it when necessary and even backing up if the need arose. Controlling the teleprompter speed needed to be as easy as possible requiring very little cognitive demands. I found that the best way to do this was through the use of mouse hovering over the control buttons with the teleprompter speed increasing or decreasing as the mouse moved right or left. This means that no mouse clicking is needed, the sound of which would likely have been picked up by the microphone.

Code Examples

The most important code is in the two green buttons. Here is the code for the one on the right, the one that propels the text in the prompter forward:

1:  global varScrollPosition   
2:  on mousewithin  
3:    set the locktext of field "prompter" to true  
4:    if varScrollPosition > the scroll of field "prompter"+2 then exit mousewithin  
5:    put item 1 of the mouseloc into varMouse  
6:    put ((varMouse-700)/25)+5 into varScrollSpeed    
7:    add varScrollSpeed to varScrollPosition  
8:    set the vScroll of field "prompter" to varScrollPosition  
9:  end mousewithin  
10:  on mouseleave  
11:    set the locktext of field "prompter" to false  
12:  end mouseleave  

The variable "varScrollPosition" controls the scrolling of the field "prompter." The variable "varScrollSpeed" controls the speed of the scrolling. The only tricky part of this code is shown in line 6. If you watch the video, you'll notice that the scrolling rate increases as the hovering mouse moves toward the right of this button. I wanted the range of scrolling speed to be from 5 to 25 pixels where 5 is very slow and 25 is very fast. The left edge of this button is 700 pixels to the right of the app window. So, I had to subtract 700 before dividing the location of the mouse by 25. This created a nice sliding scale from 0 to 25. I then added 5 so that the 5 would be the minimum scrolling speed when hovering over this button.

varScrollPosition needs to be a global variable so that the position of the scroll bar is maintained throughout the session.

I locked the text during scrolling, but unlocked it when not scrolling. This allows the user to edit the script while rehearsing.

Line 4 determines if varScrollPosition goes beyond the bottom edge of the field.  I couldn't find a system variable to tell me the range of the scrollbar, but I found system variable "scroll" that returns the current scroll position. It tops out once the scroll bar hits the bottom. So, the variables varScrollPosition and scroll are virtually the same until the scrolling reaches the bottom of the scroll bar. So, I plugged in this little formula to stop increasing varScrollPosition once it is more than 2 pixels more than scroll. 

The other options were all very simple to code. As one example, here's the code for the button that increases the vertical height of the teleprompter field:

1:  on mouseup  
2:    put the height of field "prompter" into varHeight  
3:    put the top of field "prompter" into varTop  
4:    add 10 to varHeight  
5:    set the height of field "prompter" to varHeight  
6:    set the top of field "prompter" to varTop  
7:  end mouseup  

This adds 10 pixels to the height of the field. I keep the top of the field fixed, so as the height increases, the length is added to the bottom.

All of the other options were equally simple to code to manipulate the format of the teleprompter field.

My First Two Designs Were a Disaster

I really like the simplicity of the final design in the way that it controls the system variable "vScroll." It contrasts mightily with my first two attempts. I had either forgotten or didn't know about the system variable "vScroll." So, I had concocted some very heavy handed approaches to getting the scrolling to work. I'm not going to even try to give the details, but here is a brief explanation.

I began by breaking down the script into individual characters. Line breaks would be added anytime a period was detected. I also figured that the user would want to insert line breaks at various places, so I added a search for the forward slash "/". I figured only three lines of the script would need to be shown at one time, so I created three variables, varSentence1, varSentence2, and varSentence3 to contain each. The idea was to create a type of cascading approach where the fourth sentence would become sentence 3, which would become sentence 2, which would become sentence 1. This would continue until the end of the script. I actually got it to work, but it was very hard to read as the sentences jumped abruptly from line to line. 

I could go on as there was more ugliness in these first two designs. Now you might think I'm embarrassed by such poor initial designs, but no, far from it. I've learned that the best designs don't appear magically at the first go. They take time to reveal themselves. The only way to finally get them to show up is to start on the journey and then always be on the lookout when you finally stumble across them. Yes, good - even great designs - almost always start off with failure.



Sunday, May 7, 2023

AI: The Future of Programming?

Like just about everyone else, I've been exploring ChatGPT to figure out if it is a curse or a blessing. All of the experts seem to agree it's both. At universities like mine, there has been quite a bit of gnashing of teeth over the worry that students will use it to cheat. This is a real concern, though of course cheating didn't start with ChatGPT. Still, it is a bit unnerving to type a simple request into ChatGPT, such as "Write a 3 paragraph essay on what were the major causes of the American Civil War" and then watch the AI engine produce a very convincing essay neatly matching your content and length requirements. 

Of course, worries about the impact of new technologies on education and society are not new. My first encounters with educators' worries about technology were with the advent of the calculator and then spellchecking. I also recall reading some historical literature claiming that Gutenberg's printing press took away our memories. Somehow, we have grown use to these cognitive aids and don't question their value anymore. Besides, I think education's attempts to either thwart or stall the use of a new technology is best characterized as a "cat and mouse" game. In the end, the mouse always wins. So, rather than look to how to stop the technology at education's door, I think a better approach is find ways to harness it to support students and other educational aims.

Education has also long been ambivalent about what Donald Norman referred to as "knowledge in the head" versus "knowledge in the world." That is, we seem to think that the purpose of education is to require students to commit to long-term memory everything they would ever need to know or do. A better approach is to look at ways to form partnerships between what one knows and what one can get from interactions with the world. Anyone who ever put their car keys on top of the grocery list to make sure they didn't forget to take it as they walked out the door in the morning understands the value of knowledge in the world. One can go to bed relaxed about not needing to remember to take the list in the morning because when you are running late the next day and reach for your car keys, the list will be there to remind you. 

LiveCode and AI

Alright, enough philosophy, let's get down to business. The purpose of this post is to share how the AI of ChatGPT has helped me to do some programming. Amazingly, ChatGPT knows all about LiveCode. A couple of weeks ago I tried it out by requesting a bunch of examples, such as "Write a program that sorts a list of the 50 states in alphabetical order using the LiveCode programming language." ChatGPT wrote out some very nice code for me to then insert into buttons and cards. However, I already knew how to do that task. I tried some trickier requests, such as "Write a stopwatch program that counts the seconds and allows the user to pause the counting at any time using the LiveCode programming language." ChatGPT again had no trouble doing so, and it even gave me a nice explanation of the various handlers it created in the code (see the appendix at the bottom of this post for the ChatGPT output). Of course, I had to know how to create the buttons and cards and also know how to put the code provided by ChatGPT into them.

So, wouldn't it be nice if there was an AI program that could be embedded into LiveCode that not only could write the code, but also create all of the various LiveCode objects with the code already comfortably nestled inside? Well, that's exactly what the LiveCode company is wanting to do by eventually offering a new programming platform called Xavvi. I recommend watching the video by Kevin Miller as he explains why and how LiveCode needs to move in this direction. He repeated a stark prediction that in about 10 years there will be two types of companies - those that have embraced AI and those that are no longer in business. It's unclear if the LiveCode company will be able to muster the resources to pull this off, but I hope they succeed. (They are running another fundraising campaign to support this move.) 

My Encounters this Weekend with ChatGPT

This past weekend, I had my own epiphany that gives support to Kevin's opinion of the need to move in the direction of using AI to support app development. Interestingly, it doesn't involve LiveCode, but PHP. I needed to do some updating of my web site that supports my Q sort project, now in it's eighth year. I built a Q sort app for the project with LiveCode, but the supporting web site uses PHP and mySQL to store instructor credentials, Q sort definitions, Q sort activities, and Q sort data. However, I've never been very conversant with PHP. When I began my foray into creating database-driven web sites, I used the built-in PHP server behaviors of Adobe Dreamweaver. (Please don't laugh. It was really wonderful.) I was able to build and maintain full CRUD (Create, Retrieve, Update, and Delete) web sites very quickly with very little need to know PHP code. Of course, I had to sometimes tweak the code or learn how to extend the code, but I could easily find some help and examples online. But I never learned PHP well enough to code in it myself from scratch and I was fine with that. My goal was to be productive and I didn't have the time to learn how to code in PHP from scratch. Unfortunately, Adobe took away all of those behaviors from Dreamweaver a few years ago, so I've been faced with coding PHP on my own. Trouble is, I only poke around in PHP every few months or even years. So, each time I've largely forgotten most of what I had learned previously. And I really needed to get those updates for my Q sort web site done now. Alas, what to do?

Well, I decided to see if ChatGPT could help, and help it did. I was able to deploy several new web pages that successfully connected to the mySQL database with code written entirely by ChatGPT. The hard part was figuring how how to appropriately prompt ChatGPT to do it. That is, what instructions did one need to give it? Here is one example of a simple request I gave ChatGPT that resulted in perfect code for a completely new web page that retrieved data from the database and displayed it in a table:

Write the HTML code for a web page with PHP from a mySQL database DBLIVECODE using mySQL from the table qsort_data with the database username NAMENAME and the password is PASSWORDPASSWORD . Put the database in a html table. 

Here's the code that ChatGPT generated, which I was able to easily copy and paste into a web document (with the private information clearly altered in all caps):

 <!DOCTYPE html>  
 <html>  
 <head>  
      <title>MySQL Table to HTML Table</title>  
 </head>  
 <body>  
      <?php  
           // Database connection information  
           $host = 'localhost'; // or IP address of the server  
           $dbname = 'DBLIVECODE'; // name of the database  
           $username = NAMENAME; // database username  
           $password = 'PASSWORDPASSWORD'; // database password  
           // Create a new MySQLi object  
           $conn = new mysqli($host, $username, $password, $dbname);  
           // Check for errors  
           if ($conn->connect_error) {  
                die("Connection failed: " . $conn->connect_error);  
           }  
           // Define the query to retrieve data from the table  
           $sql = 'SELECT * FROM qsort_data';  
           // Execute the query  
           $result = $conn->query($sql);  
           // Check for errors  
           if (!$result) {  
                die("Query failed: " . $conn->error);  
           }  
           // Display the data in an HTML table  
           echo '<table>';  
           echo '<tr><th>Column 1</th><th>Column 2</th><th>Column 3</th></tr>';  
           while ($row = $result->fetch_assoc()) {  
                echo '<tr>';  
                echo '<td>' . $row['q_topic'] . '</td>';  
                echo '<td>' . $row['q_code_activity'] . '</td>';  
                echo '<td>' . $row['q_data'] . '</td>';  
                echo '</tr>';  
           }  
           echo '</table>';  
           // Close the database connection  
           $conn->close();  
      ?>  
 </body>  
 </html>  

Again, the only hard part was figuring out what exactly to tell ChatGPT to do. Obviously, some knowledge of databases, PHP, and HTML was required on my part in order to get ChatGPT to give me what I needed. But, only a little trial and error was necessary to get ChatGPT to deliver the goods. Over the weekend, I made other requests to ChatGPT, such as as for code to update specific database tables and records. 

I'll treat AI like I treat GPS

ChatGPT and I are now "partners" in web development. However, I don't want to just blindly follow what it gives me. I want to learn as I go with the hope of putting some of the knowledge gained in my head and not just in the world. I think this relationship will be similar to how I use GPS apps on my phone. I always want to know where I'm going and will never just follow a GPS blindly. I like to map out my route to get the big picture of of my trip. But when I'm in a big city (like I was in Chicago a few weeks ago), it is wonderful to have the GPS guide me moment-to-moment in taking key exits and getting in the correct lanes as I anticipate making the next turn. I just hope that ChatGPT doesn't lead me into a blind alley.

Thursday, May 19, 2022

A Metaphor for Iterative Design: A Livecode Project to Demonstrate the Sierpinski Gasket

I had the good fortune to be invited by the students and faculty of the Learning Sciences and Mathematics and Science Education doctoral programs at BoÄŸaziçi University in Turkey to participate in a seminar on design-based research (DBR). DBR has become a particularly important research approach in the field of learning, design, and technology. I decided to use my 30 minute presentation on May 11, 2022 to focus on the importance of being a designer when engaging in DBR. That is, I think it important to take on the identity of a designer. When this happens, you take on a different view of the world that will influence and support your research.

The Importance of Iteration in Design

An important aspect of all design, including DBR, is iteration. The everyday definition of iteration is simply to repeat a process or procedure. But, I prefer the mathematical definition of iteration because I think it is closer to how iteration needs to be practiced in DBR. According to Wikipedia, iteration in mathematics is "the process of iterating a function, i.e. applying a function repeatedly, using the output from one iteration as the input to the next."

The key idea here is that the output of one iteration is used (at least partially) as the input of the next iteration. I think this distinction is important for DBR because it means you revise your intervention based on what you just learned from trying it out. Implementing this principle well leads to profound insights which a designer can use in the revision process. In this process you stop, reflect, and make explicit - by writing down - what you learned. This is followed by making a conjecture that if the design is revised in such and such a way, then something better will happen the next time the design is tested (e.g. more learning, more motivation, etc.) If no improvement is found, then that conjecture is refuted and a new conjecture must be constructed. All of this is being documented along the way in DBR. These conjectures should be based in the formulation and evolution of a local or "humble" theory generated by the designer/researcher. This is the heart of the DBR process. 

A Fun Example of Iteration

Rather than just talk about this on May 11, I thought I'd show a simple mathematical example as both a literal (it really shows iteration in mathematics) and metaphorical example (within DBR) of iteration. Consider the following:

  1. On a sheet of paper, draw three dots anywhere you like. These become the vertices of a triangle. I've drawn these in red in the figure below.
  2. Pick a random spot on the paper and draw another dot. I've drawn this in black on the left side of the figure below.
  3. Pick one of the three red dots at random. Let's say the bottom-right red dot was chosen.
  4. Draw a dot at the midpoint of the black dot and the randomly chosen red dot.
  5. Repeat steps 3 and 4 using the previously drawn black dot for step 4.


What do you think you will get after lots of dots, say 1000, 5000, or 10,000?

Some people guess that the piece of paper will just get filled with dots. Others guess that the triangle formed by the red dots will just fill up. Another guess is that a circular pattern will emerge.

The actual result is quite surprising. Here is a short video of a LiveCode project I built to demonstrate this example:



(I don't know about you, but I find the erasing of the dots almost as mesmerizing as the emergence of the design.)

Here is the result drawn with 5000 small dots:

This is a special fractal known as the Sierpiński Gasket. It infinitely repeats the triangular shape. That is, if we were able to keep drawing an infinite number of dots with an ever-increasing fine point, the shape would continue to repeat itself to infinity no matter how much we zoomed in or magnified any portion of the figure. It is a most surprising result from such a simple set of rules.

But, my point in the seminar was not to teach anyone about fractals, but rather to use this example in an entertaining way to make some important points, metaphorical though they might be to process of iteration within DBR:

  • The process of iteration, though a seemingly simple idea, leads to insights of otherwise hidden patterns or deep structures for how to revise the design of a learning intervention.
  • The process of iteration can bring order and understanding to the otherwise complex activity of designing for learning.
  • Despite the apparent sense of order, there is still an infinite range of possible outcomes for our designs. That is, the experience of any one person when they use one's design will be unique for that person. 
I could probably come up with lots of more metaphorical lessons given that I never "met-a-phor I didn't like," but like all metaphors, there are limits before the metaphor breaks down. Obviously, iteration within DBR is not a mathematical function, but I hoped this example would pique the audience's curiosity and interest in the principles above.

Building the Project in LiveCode


This is actually a project I built about 30 years ago using HyperCard, the ancestor of LiveCode. My original HyperCard program harnessed the graphics tools to actually draw dots on the screen. That approach still works in LiveCode, but it is frustratingly slow. So, I redesigned the approach to copy and paste the graphical object of a black dot within a loop that follows the rules above.

Here's the LiveCode interface:


On another card titled "resources" I created two graphic objects for the two black dots used in the program (medium and small) using the oval graphic tool:

The three red dots are also drawn with the oval graphic tool and are titled "triangle1," "triangle2," and "triangle3". The "Start" button contains the code to draw the fractal:
1:  global gp,gx,gy,bx,beginy,x,y,  
2:  global varLoops, varPointName  
3:    
4:  on mouseUp  
5:    put 0 into field "counter"  
6:    put field "loops" into varLoops  
7:      
8:      
9:    wait 1 second  
10:      
11:    //Game Board  
12:    --choose first "starting point" at random   
13:    put the random of 1000 into x   
14:    put the random of 500 into y   
15:    copy graphic varPointName on card "resources" to this card  
16:    hide it  
17:      
18:    set the location of it to x,y  
19:    show it  
20:    put x into gx   
21:    put y into gy   
22:      
23:    //Play the Game  
24:    Repeat with i = 1 to varLoops  
25:     if i < 101 then  
26:       put i into field "counter"  
27:     else  
28:       if (i/100)-trunc(i/100)=0 then put i into field "counter"  
29:         
30:     end if  
31:       
32:     --choose one of the three "game points" at random   
33:     put the random of 3 into gp   
34:     if gp = 2 then   
35:       put item 1 of the location of graphic "triangle2" into bx  
36:       put item 2 of the location of graphic "triangle2" into beginy  
37:     end if   
38:       
39:     if gp = 1 then   
40:       put item 1 of the location of graphic "triangle1" into bx  
41:       put item 2 of the location of graphic "triangle1" into beginy  
42:     end if   
43:       
44:     if gp = 3 then   
45:       put item 1 of the location of graphic "triangle3" into bx  
46:       put item 2 of the location of graphic "triangle3" into beginy  
47:        
48:     end if   
49:       
50:     --draw the midpoint of the "starting point" and "game point"   
51:     put (bx+gx)/2 into x   
52:     put (beginy+gy)/2 into y   
53:     put round (x) into x   
54:     put round (y) into y   
55:     copy graphic varPointName on card "resources" to this card  
56:     set the name of it to varPointName&i  
57:     set the location of it to x,y  
58:       
59:     wait 1 ticks  
60:     --make the midpoint the new "start point" and repeat   
61:     put x into gx   
62:     put y into gy   
63:       
64:    end repeat   
65:      
66:  end mouseUp  

Here are brief explanations of key lines of code:

Lines 6 takes the number the user enters and stores this in the variable "varLoops." This determines how many dots will drawn on the screen.

Lines 11-21 draw the first black dot at a random point on the screen. The coordinates of this dot's location are stored in the local variables gx and gy.

Lines 24-64 is the repeating loop that does all of the work.

Lines 24-28 count up the number of dots drawn so far and displays this on the screen. The act of updating the field with this number can slow the whole program down considerably, so I figured out some code to only show the numbers 1-100, then increments of 100 thereafter.

Line 33 chooses a number from 1 to 3 at random. Lines 34-48 takes this random number and chooses one of the three red dots accordingly.

Lines 51-57 figure out the midpoint of the randomly chosen red dot and the last black dot and put a new black dot at that location. Line 55 copies and pastes the black dot from the card "resources" to this card, then moves it to the coordinates of the midpoint. Notice, by the way, that line 56 sets the name of this newly copied and pasted graphic object as "varPointName&i". The local variable i is chosen at the start of the repeating loop in line 24 as the counter for the loop. The "&" is the concatenation symbol which is used to "join" two sets of characters. So, the 56th dot copied and pasted will have the name "varPointName56." This is important because the "Erase" button will use these names to erase each dot in sequence.

Line 59 slows down the drawing of the dots just a little. A "tick" simply 1/60 of a second. I wanted the fractal to "reveal itself" quickly, but not too quickly, so as to keep the audience wondering what was being revealed. Slowing down the drawing of the dots further might be a good idea, depending on the audience and context.

Notice that lines 35-36, 40-41, and 45-46 refer to the location of each of the three vertices. I made each vertex "grabbable" so that I could move each anywhere I wanted on the screen. Here is the simple code embedded in each red dot that makes this possible:

on mousedown
   grab me
end mousedown

Finally, the "Erase" button simply deletes all of the graphics objects containing each of the dots. Here is the code for the button "Erase:"

1:  global varLoops, varPointName  
2:    
3:  on mouseup  
4:    put 0 into field "counter"  
5:       
6:    repeat with i = 1 to varLoops  
7:     put varPointName&i into varName  
8:     delete graphic varName  
9:    end repeat  
10:      
11:    delete graphic varPointName  
12:      
13:  end mouseup  

Lines 6-9 repeat for as many dots were drawn on the screen, except for the very first dot. Each is erased at line 8. Recall that i is the counter for the repeat statement, so if there are 100 dots, dot 56 will get erased on the 56th loop. The dot at that loop would be named "varPointName56" at that point in line 7, which is then stored in the local variable varName. Line 8 commits the deed and deletes that graphical dot. What makes the resulting animation mesmerizing, I think, is that the dots disappear in the same order in which they first appeared.

Final Thoughts


There are lots of possible "next projects" using the above code as a foundation. What would appear if I use 4, 5, or 6 vertices instead of 3? I'm sure the answer is already known and published somewhere by a mathematician and all I have to do is Google it. But, it is fun to "play mathematician" occasionally to find out for myself. I would also like to explore further the idea of using the polygon tool to create this figure. Then, I could play "connect the dots," perhaps with a random color for each line segment. That sounds like a nice blending of mathematics and art.

I end with thanking the students and faculty of the Learning Sciences and Mathematics and Science Education doctoral programs at BoÄŸaziçi University in Turkey for inviting me to share some thoughts about design-based research. It gave me a good excuse to do some computer programming in LiveCode for fun.



Saturday, April 16, 2022

I Made a Wordle Cheating Program (by Mistake)

I recently was a guest speaker in an educational game design class at Old Dominion University. In preparation, I thought I'd check out Wordle given that it has been very popular recently. The "official" Wordle site is at the New York Times and it only provides a new word once a day. However, here is a good online version where you can play unlimited games.

The game's simplicity intrigued me - the graphic design just involves letters and simple colored boxes with a very simple user interface. It's quite a contrast when compared to other popular online games involving 3D graphics and virtual worlds. According to Wikipedia, Wordle was developed by Josh Wardle who eventually sold the game to the New York Times for "an undisclosed seven-figure sum." Hmm, I said to myself. I once created a word game called "Crack the Code." Maybe I have a seven-figure sum game I need to release to the world. Heck, I'd settle for a six-figure sum sale. As it turns out, I did "release this to the world" back in 2013 in a blog post. Unfortunately, my game is not much fun to play. How do I know? Well, when I asked people to play it and asked them how they liked it, they replied "Sorry, Lloyd, it's just not very fun to play." Pretty solid evaluation data there. The question of "What makes a game fun to play?" is actually a very interesting and profound question. So, it's worth thinking about why Wordle is so captivating.

Interestingly, even though I have much interest in games, I do not identify myself as a "gamer," mainly because I'm legitimately afraid of getting addicted and I just don't have time for that. Well, after playing Wordle for a short time, I was hooked. As I played it, it was clear to me that the first and second words you enter at the start of the game are very important. It turns out there are lots of web sites and videos that propose the best words to use. I've settled on "audio" as my favorite first word given that it contains so many vowels. Other favorites are "radio" and "audit." Here's an interesting video that provides some interesting statistics and advice for choosing a first word - bottomline: don't pick "fuzzy."

As I continued to play the game, I began to wonder how to determine the best way for myself to choose the first and second words. Given that the game is based on text processing, I thought this would be a perfect LiveCode project. So, last Sunday afternoon, I spent about two hours to quickly produce such a LiveCode program. A day or two later I thought this would be a good program to share with others on this blog, so I spent another two hours or so revising the user interface so others could use it. (As is typical, the time spent on the user design interface takes as long - and usually longer - than the programming for the software itself.)

Here's a screen shot of the interface:

I'll explain how this works in a moment, but the important point I want to make is my sole purpose in creating this app was only to gather statistics on the best words to choose at the beginning of the game. However, it turns out that my app also is the perfect tool to cheat at Wordle! The reason is that my program continues to whittle down the possible word matches as you go. Consequently, I can pretty much just choose a word at random at each turn of the game and win. As someone quickly asked me when I told them about this, doesn't that take the fun out of the game? The answer is yes. Of course, for me, I continue to find it quite exhilarating to see my little program chug along to beat the game.

Obviously, one needs a good list of five letter words from which to search for good and bad matches. I used the word list available at this site: https://eslforums.com/5-letter-words/

However, I really need to find a more complete word list because after using my app to play Wordle, I found my list was missing these common words: torte, motel, moron, spank.

Ok, here's a short video of me using the program as I played a round of Wordle.


The main code that makes this all work can be found in the "Search Word List" button. Here's the code:

1:  on mouseup  
2:    put empty into field "matches"  
3:      
4:    put the number of lines in field "word list" on card "resources" into L  
5:      
6:    repeat with i = 1 to L //word list loop  
7:     put false into varSkipWord  
8:     put line i of field "word list" on card "resources" into varWord  
9:       
10:     //Check first for matched letters in correct spot  
11:     put line 1 of field "first" into varFirstMatch  
12:     if field "first" is not empty then  
13:       if char 1 of varWord <> varFirstMatch then next repeat  
14:     end if  
15:     put line 1 of field "second" into varSecondMatch  
16:     if field "second" is not empty then  
17:       if char 2 of varWord <> varSecondMatch then next repeat  
18:     end if  
19:     put line 1 of field "third" into varThirdMatch  
20:     if field "third" is not empty then  
21:       if char 3 of varWord <> varThirdMatch then next repeat  
22:     end if  
23:     put line 1 of field "fourth" into varFourthMatch  
24:     if field "fourth" is not empty then  
25:       if char 4 of varWord <> varFourthMatch then next repeat  
26:     end if  
27:     put line 1 of field "fifth" into varFifthMatch  
28:     if field "fifth" is not empty then  
29:       if char 5 of varWord <> varFifthMatch then next repeat  
30:     end if  
31:       
32:     //Second, check for letters to exclude  
33:     put the number of lines in field "exclude" into M  
34:     repeat with j = 1 to M //use excluded letters loop  
35:       put line j of field "exclude" into varLetterToExclude  
36:       if varWord contains varLetterToExclude then   
37:        put true into varSkipWord  
38:        exit repeat //no need to keep looking, just exit out  
39:       end if  
40:     end repeat //END use excluded letters loop  
41:       
42:     //Third, check for letters to include  
43:     put the number of lines in field "include" into N  
44:     repeat with k=1 to N //use included letters loop  
45:       put line k of field "include" into varLetterToInclude  
46:       if varLetterToInclude is not in varWord then  
47:        put true into varSkipWord  
48:        exit repeat //no need to keep looking  
49:       end if  
50:     end repeat //END use included letters loop  
51:       
52:     //Fourth and last, check for matched letters but not yet in the right spot  
53:     put line 1 of field "notfirst" into varNotFirstMatch  
54:     if field "notfirst" is not empty then  
55:       if char 1 of varWord = varNotFirstMatch then next repeat  
56:     end if  
57:     put line 1 of field "notsecond" into varNotSecondMatch  
58:     if field "notsecond" is not empty then  
59:       if char 2 of varWord = varNotSecondMatch then next repeat  
60:     end if  
61:     put line 1 of field "notthird" into varNotThirdMatch  
62:     if field "notthird" is not empty then  
63:       if char 3 of varWord = varNotThirdMatch then next repeat  
64:     end if  
65:     put line 1 of field "notfourth" into varNotFourthMatch  
66:     if field "notfourth" is not empty then  
67:       if char 4 of varWord = varNotFourthMatch then next repeat  
68:     end if  
69:     put line 1 of field "notfifth" into varNotFifthMatch  
70:     if field "notfifth" is not empty then  
71:       if char 5 of varWord = varNotFifthMatch then next repeat  
72:     end if  
73:       
74:     if varSkipWord is false then put varWord&return after field "matches"  
75:    end repeat //END word list loop  
76:      
77:    put the number of lines in field "matches"&space&"matches" into field "matches label"  
78:      
79:    //Compute Statistics  
80:    put the number of lines in field "matches" into varMatchesCount  
81:    put line 1 of field "guess" into varGuess  
82:    put the number of lines in field "matches" into varNumerator  
83:    put the number of lines in field "word list" on card "resources"into varDenominator  
84:    put varGuess&comma&varMatchesCount&"("&round((varNumerator/varDenominator) * 100,3)&"%"&")"&return after field "statistics"  
85:      
86:  end mouseup  

I put a comment at the start of each important block of code. The key variable is varSkipWord. This variable will determine whether or not each word in the list of over 2500 words should be skipped. If skipped, the word does not match any still available word to try in the next guess. In line 7, I set this to false, meaning that unless something tells the computer otherwise, keep the word as a possible match - that is, don't skip it. At various places in the code, you'll see the code "put true into varSkipWord," such as in line 37. If varSkipWord remains false by the time the code reaches line 74, then the word is added to field "matches," which shows all of the words that are viable possibilities for the next guess.

In conclusion, this little project was perfect for LiveCode. Too bad I ruined my own Wordle game experience. I hope I don't ruin it for you.

Sunday, December 20, 2020

My Toothbrush Time App: Nearing Completion

I solved the problem I mentioned in my last post about the interval timer not working properly when the app is paused in the middle of a timing session. I completely overhauled the programming for this and the app's been working fine since. I barely remember how the original approach worked. I remember I used some (in hindsight) whacky system where I was subtracting interval seconds from the total seconds elapsed plus the amount of time paused, or something like that. It was yet another example of me programming by the seat of my pants without thinking it through ahead of time. But honestly, that's the way I like to program. I have a firm goal in mind and I just "have at it" until I reach the goal. I find this an exhilarating quest. Usually things work out fine, but occasionally - as in this case - they don't. A reasonable trade-off as far as I'm concerned.

The new approach is a simple one that occurred to me on a long walk as I was pondering the problem. It uses the the time on the clock as the starting point for figuring out when each new interval should end. So, if the next next interval is 20 seconds long and it begins when the clock reads 0:30, obviously the alert should sound at the 0:50 mark. If the person pauses the timer, the display is frozen until the Restart button is pressed. I still had to do some transposing of time based on the theSeconds function, but the logic was sound.

I had also mentioned in my previous post that the vibrate alert option seemed to really throw things off. It turned out that an extra second was added to the current interval every time the vibrate option was used. Given that the pattern of adding one and only one extra second never changed, I decided to just add the hack of subtracting a second when the vibrate option was used. I accomplished this with a variable I created named varIntervalFudge (in my mind fudge = hack).

Yeah, programming a hack rather than tracing down the actual problem is the lazy way to do things. But hey, I'm a busy guy.

Here is a review of the programming engine for this app. MainEventLoop, as its name suggests, is the main programming loop that allows the timer to run while the user is making choices, such as starting, stopping, and resetting the timer:

1:  on MainEventLoop  
2:    if varRunMainLoop is true then  
3:     lock screen  
4:     LloydStopWatch  
5:     unlock screen  
6:    end if  
7:    put the pendingMessages into tMessages   
8:    repeat for each line aLine in tMessages   
9:     if item 3 of aLine is "mainEventLoop" then cancel item 1 of aLine   
10:    end repeat    
11:    send "mainEventLoop" to me in 50 milliseconds  
12:  end MainEventLoop  

Line 4 triggers the procedure LloydStopWatch to run every 50 ms. The LloydStopWatch procedure contains the programming that operates the timer and checks on the intervals:

1:  on LloydStopWatch    
2:    put ((varIminutes1*60) + varIseconds1 - varIntervalFudge) into varInterval1  
3:    put ((varIminutes2*60) + varIseconds2 - varIntervalFudge) into varInterval2  
4:    put ((varIminutes3*60) + varIseconds3 - varIntervalFudge) into varInterval3  
5:    put ((varIminutes4*60) + varIseconds4 - varIntervalFudge) into varInterval4  
6:    if varIntervalTurn =1 then put varInterval1 into varIntervalCurrent  
7:    if varIntervalTurn =2 then put varInterval2 into varIntervalCurrent  
8:    if varIntervalTurn =3 then put varInterval3 into varIntervalCurrent  
9:    if varIntervalTurn =4 then put varInterval4 into varIntervalCurrent  
10:      
11:    put the seconds into varSecondsEnd    
12:    put (varSecondsEnd - varSecondsBegin) + varTimeElapsedPrevious into varTimeElapsed //Modified December 6, 2020  
13:    put varSecondsEnd - varSecondsBeginInterval into varTimeElapsedInterval  
14:      
15:    //Display the time elapsed  
16:    put trunc(varTimeElapsed/60) into varDisplayMinutes  
17:    put (varTimeElapsed)-(varDisplayMinutes*60) into varDisplaySeconds  
18:    if the length of varDisplaySeconds < 2 then put "0"&varDisplaySeconds into varDisplaySeconds  
19:    put varDisplayMinutes&":"&varDisplaySeconds into line 1 of field "my time"  
20:      
21:    if varIntervalCurrent = 0 then exit LloydStopWatch //Only when happens when all internal fields are 0  
22:    if varIntervalTurn = 1 and varIntervalCheck = true then //December 6, 2020  
23:     put varInterval1 + varTimeElapsed into varTimeOfNextAlert  
24:    end if  
25:    if varIntervalTurn = 2 and varIntervalCheck = true then //December 6, 2020  
26:     put varInterval2 + varTimeElapsed into varTimeOfNextAlert  
27:    end if  
28:    if varIntervalTurn = 3 and varIntervalCheck = true then //December 6, 2020  
29:     put varInterval3 + varTimeElapsed into varTimeOfNextAlert  
30:    end if  
31:    if varIntervalTurn = 4 and varIntervalCheck = true then //December 6, 2020  
32:     put varInterval4 + varTimeElapsed into varTimeOfNextAlert  
33:    end if  
34:    put false into varIntervalCheck  
35:      
36:    if varTimeElapsed = varTimeOfNextAlert then  
37:     if varIntervalTurn = 1 and varChime1 <> "vibrate" then play specialFolderPath("engine")&varChime1  
38:     if varIntervalTurn = 1 and varChime1 = "vibrate" then mobileVibrate 2  
39:     if varIntervalTurn = 2 and varChime2 <> "vibrate" then play specialFolderPath("engine")&varChime2  
40:     if varIntervalTurn = 2 and varChime2 = "vibrate" then mobileVibrate 2  
41:     if varIntervalTurn = 3 and varChime3 <> "vibrate" then play specialFolderPath("engine")&varChime3  
42:     if varIntervalTurn = 3 and varChime3 = "vibrate" then mobileVibrate 2  
43:     if varIntervalTurn = 4 and varChime4 <> "vibrate" then play specialFolderPath("engine")&varChime4  
44:     if varIntervalTurn = 4 and varChime4 = "vibrate" then mobileVibrate 2  
45:       
46:     //Hack when vibrate alert is used - December 6, 2020  
47:     //Reduce the next interval time by 1 second  
48:     put 0 into varIntervalFudge  
49:     if varIntervalTurn = 1 and varChime1 = "vibrate" then put 1 into varIntervalFudge  
50:     if varIntervalTurn = 2 and varChime2 = "vibrate" then put 1 into varIntervalFudge  
51:     if varIntervalTurn = 3 and varChime3 = "vibrate" then put 1 into varIntervalFudge  
52:     if varIntervalTurn = 4 and varChime4 = "vibrate" then put 1 into varIntervalFudge  
53:       
54:     add 1 to varIntervalTurn  
55:     //Next 3 lines account for when either interval 2 or 3 are empty; if interval 2 is empty, then code assumes interval 3 is empty (even if isn't)  
56:     if varInterval2 = 0 then put 1 into varIntervalTurn  
57:     if varIntervalTurn = 3 and varInterval3 = 0 then put 1 into varIntervalTurn  
58:     if varIntervalTurn = 4 and varInterval4 = 0 then put 1 into varIntervalTurn  
59:     if varIntervalTurn > 4 then put 1 into varIntervalTurn     
60:       
61:     put true into varIntervalCheck  
62:    end if  
63:      
64:  end LloydStopWatch   

Note the use of the variable varIntervalFudge when accounting for the vibrate alert.

Option to Save the Current Settings

One nice little feature I added was giving the user the option to save the current settings. This is really helpful if you have a main use for the app, such as teeth brushing, and want to revert to those settings after using the app to do other things. The solution to this is similar to the idea of a "cookie." The idea is to save a small data file on the iPhone that can be retrieved later. Here's a link to a good tutorial on how to set up something like this:

https://lessons.livecode.com/m/4069/l/14301-how-do-i-read-write-to-files-on-mobile

Here is the code for saving the current settings located in the button "Save current settings":

1:  on mousedown  
2:    put empty into field "appdata" on card "data"  
3:    Put "Interval Data" into line 1 of field "appdata" on card "data"  
4:    put "Interval 1,"&field "IMinutes1"&comma&field "ISeconds1"&comma&field "chime1" into line 2 of field "appdata" on card "data"  
5:    put "Interval 2,"&field "IMinutes2"&comma&field "ISeconds2"&comma&field "chime2" into line 3 of field "appdata" on card "data"  
6:    put "Interval 3,"&field "IMinutes3"&comma&field "ISeconds3"&comma&field "chime3" into line 4 of field "appdata" on card "data"  
7:    put "Interval 4,"&field "IMinutes4"&comma&field "ISeconds4"&comma&field "chime4" into line 5 of field "appdata" on card "data"  
8:      
9:    //Activate the following line to actually save the data  
10:    put field "appdata" on card "data" into URL ("file:data.txt")  
11:    wait 1 second  
12:    show image "checkmark"  
13:    wait 1 second  
14:    hide image "checkmark"  
15:  end mousedown  

Lines 4-7 put the data for each of the four intervals into the text field "appdata" on card "data."

Line 10 saves everything in field "appdata" into a text file named "data.txt"

I decided to give the user feedback that the file was saved by displaying a very small graphic checkmark. It looks better if there is a pause for one second before it is displayed. I then hide the checkmark after another second.

Here is the code in the button "Revert to saved settings" that retrieves the settings data:

1:  on mousedown  
2:      
3:    put empty into field "appdata" on card "data"  
4:    set the defaultFolder to specialFolderPath("Documents")  
5:    put URL ("file:data.txt") into field "appdata" on card "data"  
6:      
7:    if field "appdata" on card "data" is empty then  
8:     put "No saved settings" into field "notes"  
9:     put "0" into field ISeconds1  
10:     put "0" into field ISeconds2  
11:     put "0" into field ISeconds3  
12:     put "0" into field ISeconds4  
13:     put "0" into field IMinutes1  
14:     put "0" into field IMinutes2  
15:     put "0" into field IMinutes3  
16:     put "0" into field IMinutes4  
17:     put "Chimes" into field "chime1"  
18:     put "Chimes" into field "chime2"  
19:     put "Chimes" into field "chime3"  
20:     put "Chimes" into field "chime4"  
21:     put "/sounds for toothbrush app/chimes.wav" into varChime1  
22:     put "/sounds for toothbrush app/chimes.wav" into varChime2  
23:     put "/sounds for toothbrush app/chimes.wav" into varChime3  
24:     put "/sounds for toothbrush app/chimes.wav" into varChime4  
25:    else  
26:     put item 2 of line 2 of field "appdata" on card "data" into field IMinutes1  
27:     put item 3 of line 2 of field "appdata" on card "data" into field ISeconds1  
28:     put item 4 of line 2 of field "appdata" on card "data" into field "Chime1"  
29:       
30:     put item 2 of line 3 of field "appdata" on card "data" into field IMinutes2  
31:     put item 3 of line 3 of field "appdata" on card "data" into field ISeconds2  
32:     put item 4 of line 3 of field "appdata" on card "data" into field "Chime2"  
33:       
34:     put item 2 of line 4 of field "appdata" on card "data" into field IMinutes3  
35:     put item 3 of line 4 of field "appdata" on card "data" into field ISeconds3  
36:     put item 4 of line 4 of field "appdata" on card "data" into field "Chime3"  
37:       
38:     put item 2 of line 5 of field "appdata" on card "data" into field IMinutes4  
39:     put item 3 of line 5 of field "appdata" on card "data" into field ISeconds4  
40:     put item 4 of line 5 of field "appdata" on card "data" into field "Chime4"  
41:       
42:     repeat with i = 1 to 4  
43:       put "chime"&i into varI  
44:       if field varI is "Vibrate" then put "vibrate" into varChimeLocal  
45:       if field varI is "Applause" then put "/sounds for toothbrush app/applause.wav" into varChimeLocal  
46:       if field varI is "Bell" then put "/sounds for toothbrush app/bell.wav" into varChimeLocal  
47:       if field varI is "Bicycle Bell" then put "/sounds for toothbrush app/bicycle_bell.wav" into varChimeLocal  
48:       if field varI is "Chimes" then put "/sounds for toothbrush app/chimes.wav" into varChimeLocal  
49:       if field varI is "Ding" then put "/sounds for toothbrush app/ding.wav" into varChimeLocal  
50:       if field varI is "Laser" then put "/sounds for toothbrush app/laser_x.wav" into varChimeLocal  
51:       if field varI is "Oops" then put "/sounds for toothbrush app/oops.wav" into varChimeLocal  
52:       if field varI is "Slide Whistle" then put "/sounds for toothbrush app/slide_whistle_up.wav" into varChimeLocal  
53:       if field varI is "Tada" then put "/sounds for toothbrush app/tada2.wav" into varChimeLocal  
54:       if field varI is "Trumpet" then put "/sounds for toothbrush app/trumpet_x.wav" into varChimeLocal  
55:         
56:       if i = 1 then put varChimeLocal into varChime1  
57:       if i = 2 then put varChimeLocal into varChime2  
58:       if i = 3 then put varChimeLocal into varChime3  
59:       if i = 4 then put varChimeLocal into varChime4  
60:         
61:     end repeat  
62:    end if  
63:      
64:  end mousedown  

Yeah, there is quite a bit to this script, but that's because I had to take into account the chance that no settings had yet been saved. Line 5 is the key line. It takes whatever data are saved and puts it back into the text field "appdata" on card "data." From there, I can set all of the intervals to the saved settings properly.

Playing Alerts When the iPhone is Locked

Unlike tooth brushing, many other tasks may take quite a long time. The example that comes to mind is giving a 20 or 30 minute presentation where I'd like to have (most likely vibrate) alerts with about three or five minutes to go, then with a minute left, and then a final alert with 15 seconds to go so I can get off the stage for the next person. It is easy to see that the iPhone will likely automatically lock during task like this. I have my iPhone auto-lock after five minutes if not used. I want to be assured that the alerts will come whether the phone is locked or not. Unfortunately, right now although the timer keeps working, the alerts never sound.

I did a search on the LiveCode forums about this and found what should be the solution, the command iphoneSetAudioCategory. Here is a good web page with information about this command:

https://livecode.fandom.com/wiki/Iphonesetaudiocategory

The problem should be solved just by adding the command iphoneSetAudioCategory "playback" to my code. Unfortunately, it isn't working.  I'll continue playing around with it. If I don't have any luck soon, I'll reach out to the LiveCode community in the forums.

Creating an Home Icon for the App

Despite my lack of graphic design skills - something I've written about frequently in this blog - I still enjoy the challenge of graphic design. Here is the design I've come up with for the app's icon on the iPhone home screen:


Though trite, a clock face is obviously synonymous with time. The four different colored squares give the clock face a distinctive look while also symbolizing the four opportunities to set four different times with unique alert sounds. I created this icon - and I'm almost embarrassed to admit it - in PowerPoint. Of all of the graphic tools out there, I find those in PowerPoint to be the most intuitive and simple to use. It's then easy to copy and paste the graphical object into Photoshop for exporting as a PNG file. My motto is always to use the tools that work for me and get the job done.

I also started toying around with some better names for this app other than "Lloyd's Toothbrush Timer App." Brushing my teeth was just the inspiration for the need for this app after all. I have a very bad track record of naming my apps, hence the names "Lloyd's Video Analysis Tool" and "Lloyd's Q Sort Tool." So, yes, I may just wind up calling this something like "Lloyd's Interval Timer App."

But, in an effort to force some extra drops of creativity, I thought I might name this the "This, Then That Timer." I played around with the alliteration of the four Ts in an earlier version of the app's icon, but none of those looked very good. Still, I think this name has potential. Of course, the boring - but descriptive - name of "Multiple Interval Timer" might win the day.

Just a Few Remaining Tasks

Very little is left on my punch list for this app other than solving the auto-lock issue just described. Here is the short list of the remaining tasks:

  • Continue to improve the graphical interface.

  • Improve the timer display to also include the number of seconds until when the next alert will be sounded. A "countdown" displays like this seems like a good idea to me.

So, look for at least one more blog post about this project.


Saturday, December 12, 2020

My Toothbrush Timer App: On My iPhone and Working (Well Enough for Now)

I've made much progress on my app in the past week. In fact, for the past few days I've been brushing my teeth with the help of my iPhone sitting on the edge of the bathroom sink running my app. It works great, just like I had hoped. I can "set it, start it, then forget it" as I start brushing. 

I like to set it to provide some gentle chimes after 50 seconds to let me know the first minute is just about up. Ten seconds later, at the one minute mark, I set it to ring a soft bell to let me it is time to switch "levels" of brushing from bottom to top. Chimes again alert me 50 seconds later to let me know that I have just 10 more to brush. It's actually quite satisfying to hear the final "tada!" alert just as the toothbrush automatically turns itself off. I also find it great for my one minute of mouthwash swishing immediately after brushing. 

Of course, with progress comes many lessons learned. This post contains a few highlights of what I learned over the past week.

Trouble Getting Any Sounds to Play on the iPhone Simulator or the Real iPhone

For a few days, I was baffled by the fact that although the app would play the sound alerts on my Macintosh during development, they would not play on either the iPhone simulator or later when I figured out how to transfer the app to my iPhone. At first, I just figured it was a problem with the simulator, so I ignored the problem. But, when the sounds failed to play on the iPhone itself, I knew something was wrong. I had been consulting this LiveCode tutorial:

https://lessons.livecode.com/m/4069/l/12353-how-do-i-play-sounds-on-an-ios-device

The problem turned out to be that I had failed to provide the full path to the sounds. I had put the sounds in a folder on my Macintosh called "Sounds for Toothbrush App." This folder is at the same level as the app. I used the "copy files" option in the standalone settings, just as the tutorial described. I didn't realize that the path shown in that window had to match the code exactly. Here's a screenshot of the copy files window:


Here was the code I was using:

play specialFolderPath("engine") & "/bell.wav"

The sounds only started to play once I changed the code to the following:

play specialFolderPath("engine") & "/sounds for toothbrush app/bell.wav"

Note: The tutorial uses "resource" instead of "engine" for the code, but I had used "engine" in a previous app I made back in 2013, so I switched to it instead. Not sure if it makes a difference.

Testing the App on an Actual iPhone

I had been dreading this part because I knew it would require me to crawl back into the very frustrating world of the Apple Development Center. I vaguely recall needing all sorts of certificates and something called a provisioning profile. I also recall having to tell Apple the identifying information for the iPhone I wanted to test the app out on. I figured this was going to be a dark day.

Fortunately, I kept good notes the last time I created an iPhone app back in 2013. I found these notes on my Macintosh, dusted them off (metaphorically speaking), and began to follow them blindly. Lo and behold, they pretty much guided me through the entire process. There was one trouble spot. In my old notes (which, of course, were new at the time I wrote them), I got to a point where I had written "Not sure what to do next! So, I check my old notes..." Remember, these "old notes" were old back in 2013. So, I had to figure all that out. But, I updated all of these notes as I went. Here is a link to those updated notes - maybe someone out there in the blogosphere will find them useful. (Beware: This are written only well enough for me to fully understand. All bets are off if you will find them useful.) But, really, I figure I'll be the one needing these notes again when I likely make my next iPhone app seven or eight years from now.

Interestingly, I first tried getting my app to work on an old iPhone SE running an up-to-date iOS, but I had no luck. I then tried to get the app on my iPhone 6S and it worked fine. I think the problem has to do with the age of the phone and the version of Xcode I'm using. 

Updating the App's Interface

I also took some time to update the graphical design of the app. My goal was just to reduce some of its ugliness. Here's a screenshot of the current app:

Current Problems to Solve

After much testing, it's clear that the interval timer is not working properly when the app is paused in the middle of a timing session. I had mentioned this problem in an earlier blog post. At that time, I thought the problem was minor - I called is likely a "rounding error" problem. But, it's much more serious than that. Also, when I use the vibrate alert instead of an audible sound, the timer really gets screwed up. So, I realize I have to completely overhaul the programming of the intervals. I have some ideas of how to go about this, but I'll save that for a future post.

For now, I at least have an app on my iPhone that helps me brush my teeth very evenly.