Message Boards Message Boards

Chicken Scratch: The Game

GROUPS:

two screen shots of Chicken Scratch

Chicken Scratch is a game, a sort of academic trivia. Students at two high schools enjoyed it so much they lined up during lunch to play. Both schools held Chicken Scratch tournaments each semester.

At first I wrote Chicken Scratch in HyperStudio to help students study for the Academic Decathlon competitions. When I changed schools, I rewrote it in Runtime Revolution with more general questions. Both versions had 300 or so hand-crafted questions.

I retired Chicken Scratch when that second school closed in 2010. As popular as it was, it did have a prominent shortcoming. You started to memorize the answers if you played enough times.

Then came the Wolfram Language. It didn't take me long to realize that I could resurrect Chicken Scratch and fix the shortcoming. For the past year or so I've been writing a new Wolfram Language version of the game. You get the first peek.

Chicken Scratch now has 15 categories of questions: Numbers, Algebra, Geometry, Graphs, Physics, Astronomy, Chemistry, Geography, Anatomy, Biology, Language, Literature, History, Art, and et Cetera (for questions that don't fit other categories). Any given tableau of choices displays nine of these categories plus Grab Bag, which chooses a category randomly and multiplies the question value by 1.5.

Each category currently has 15 questions in it, but here's the kicker. Every question has a high degree of random variability! For instance, a player chooses the Biology category and gets the following question:

Taxonomically, which of these pairs of animals is most closely related?

  • pacu & Portuguese man o' war
  • whale remora & armadillo girdled lizard
  • sea scallop & streaked tenrec
  • moose & great star coral

Those eight animals though were taken randomly from a database of 150 animals which I built using Wolfram's SpeciesData[]. So if you just consider the correct answer, this one question has 150C2 variations, or 11,175... even more if you figure that the animals in the distractors are also chosen randomly. There's no way players are memorizing the answers now!

In a school setting, the power of such a question is that it provokes thought and discussion. After the students attempt to answer the question above, for instance, I'd explain that remoras and lizards both have backbones, inching all who hear the explanation a bit closer to understanding how taxonomy works. A student might object, “But coral isn't an animal,” providing another teachable moment.

The question, of course, uses the taxonomic properties of each animal from SpeciesData[] to determine the correct answer, so all I have to do is click on the answer the student chose. The game adds the points and proceeds to a new tableau of choices, until one team has reached 300 (or the other team has dropped to -300).

This example is text-based, but about half of the questions feature graphics, also randomly variable. I've included a few screen shots of questions at the end of this post.

I'm enthusiastic about this topic; I could go on and on. That won't make for a readable post though, so I will stop here. If there is enough interest (comments, votes, visits), then I will add to this discussion by sharing some code and explaining how I built Chicken Scratch's interface.

Chicken Scratch history question

Chicken Scratch graphs question

Chicken Scratch astronomy question

I wasn't sure whether to make this into a new discussion or just add on to the original. I decided to add on.

Chicken Scratch: The Interface

Here I'll explain the game's interface design. I am not a Wolfram guru. I still use some procedural structures like If[] in my code. My goal was to make Chicken Scratch work well, even if the code is sometimes ugly. I don't see similar apps on the web made in the Wolfram language, so maybe this will help explore some new territory.

In general, the design is this:

  • Present a button (cover art).
  • When the button is clicked, replace it with ten buttons (categories).
  • When one of the ten buttons is clicked, replace all with a question and four buttons (choices).
  • When a choice button is clicked, change the score and replace all with ten buttons again.
  • Keep doing this till someone wins.

Chicken Scratch's interface is one block of code, the last line of which is the only one to return an output. I have included that entire block of code at the bottom of this post. You may want to refer to it while you read my description.

The function call that starts the whole game, newGame[] does this:

  • Makes a button of a title image the size of the whole game;
  • Tells the button to trigger the function start[] when clicked;
  • Puts that button into a variable called game.
  • Places some auxiliary buttons below the game, which I'll be ignoring in this post.

Note that game is dynamic. That's because we will be replacing the button that's inside of game with some other content, and Dynamic[] will make that change immediately visible.

The function start[] does this:

  • Zeros out both scores;
  • Picks a player to go first (with the help of upΔ[]);
  • Changes the content of game to a banner and a body;
  • Triggers a function called newQ[].

Note that body is also dynamic so that changes in body will display immediately.

The function newQ[] does this:

  • If somebody won,
    • Displays a victory graphic;
    • Displays the margin of victory;
    • Makes the hand graphic point to the winner.
  • Otherwise,
    • Sets try to 1 (try = 2 is the second player's attempt);
    • Randomly assigns a dollar value for the next question;
    • Makes an image that displays the value of the next question and the word “Choose”, qValImg[0];
    • Randomly takes 9 of the 15 categories;
    • Puts 5 of the 9 categories into a column;
    • Puts the other 4 categories plus Grab Bag into another column;
    • Replaces the content of body with these two columns of categories.

So far we have a cover image that, when clicked, is replaced with a banner above two columns of category names.

Note that the category names in the columns are really buttons that call the getQA[cat_] function. I explain it later.

I initialize variables in a sort of haphazard way throughout this block of code. I know that this is not ideal, something I need to improve on. For the sake of clarity, I'll describe some of those variables now before explaining the remaining functions:

  • urlStem is just the first part of the URL of my Wolfram cloud account so I don't have to keep typing it.
  • coverPic, ...hand2Pic are the last part of the URL for each image resource.
  • ptPair[x_List] and matrix[x_List] are functions that format math expressions, not relevant to the interface.
  • grabBag is the text “Grab Bag” formatted to become part of the category list.
  • scoreBox1 and scoreBox2 are the words “Team 1” or “Team 2” over each team's score.
  • spacer1 and spacer2 helped me with the layout, shouldn't be necessary in theory.
  • banner is a row of five panes that hold scoreBox1, hand1, valueBox, hand2, and scoreBox2.
  • happySound and sadSound are audio cues for right and wrong answers.

The function getQA[cat_] does this:

  • Takes the category name from the button the player chose;
  • Picks a number, 1-15.
  • Puts the category and number together so qaFetch[which_] knows what cloud object to poll.

The qaFetch[which_] function does this:

  • Gets data from one of 225 cloud objects, one for each question type in the game;
  • Breaks this data into four pieces: the question, q; the answer, ans; the choices, mixed; and an optional graphic, pic.

Note that some questions have to send the data wrapped in Hold[], so I use ReleaseHold[] to evaluate those on the local machine. You can run ReleaseHold[] on expressions that are not wrapped in Hold[], so it works either way.

The choseRt[] function does this:

  • Plays a positive audio cue;
  • Adds the question value to the player's score;
  • Switches players (if it was the first attempt at the question);
  • Calls newQ[] to bring up the categories again.

The choseWr[] function does this:

  • Plays a negative audio cue;
  • Subtracts the question value from the player's score;
  • Halves the question value (if it was the first attempt);
  • Switches players for a second try or calls newQ[].

The function upΔ[] simply switches players and points a finger graphic at the player whose turn it is.

The functions qValImg[g_] and scrDifImg[] make graphics that display some information in the center of the banner. I went a little overboard with this and had some unnecessary fun with Wolfram's image processing functions.

The code containing all of the functions and variables I've explained thus far starts the game by replacing the cover art with ten choices, and then allows the player to choose a category, upon which some data is retrieved from the cloud. The game branches appropriately on right or wrong answers and eventually returns to a list of categories. This continues until one score is outside the range of -299 to 299.

The only part of the code I haven't explained is the If[] function beginning with If[pic===Null,...]. It formats the display of the question once Chicken Scratch gets data back from the cloud, specifically:

  • If there is no image/graphic/3Dgraphic in the data returned from the cloud,
    • Display the question and all four choices stacked in a column.
  • Otherwise,
    • If the image/graphic is narrower than 530 pixels,
      • Display the image/graphic on the right;
      • Stack the question and choices on the left.
    • Otherwise (as with timelines and such),
      • Display the question across the top.
      • Display the image/graphic across the center.
      • Display the choices in a 2×2 grid at the bottom.

Here is the code. I have removed my user number from the URL thinking it may be a security risk to post it. I'm not sure about that. Also, to run this code, I use an Enterprise license to turn it into a .cdf file. The Enterprise license is necessary because of the dynamic content.

If anyone wants to try out Chicken Scratch, contact me off list (markgreenberg@cox.net) and I'll send you the .cdf. I'd like to limit the number of people I share it with because each time the game polls the cloud for new questions it uses my cloud credits. I'd appreciate any feedback you can give.

Thanks,

Mark Greenberg

(* COMMON VARS and FUNCTIONS *)

urlStem = "https://www.wolframcloud.com/objects/user-REMOVED/";
coverPic = Import[urlStem <> "images/cs_cover"];
victorPic = Import[urlStem <> "images/cs_victory"];
winnerPic = Import[urlStem <> "images/cs_winner"];
hand1Pic = Import[urlStem <> "images/cs_hand1"];
hand2Pic = Import[urlStem <> "images/cs_hand2"];

ptPair[x_List] := DisplayForm[StyleBox[RowBox[{"(", Row[x, ","], ")"}], SpanMaxSize -> Infinity]];
matrix[x_List] := DisplayForm[StyleBox[RowBox[{"[", TableForm[x, TableAlignments -> Center, TableSpacing -> {2, 1.5}], "]"}],SpanMaxSize -> Infinity]];

qaFetch[which_] :=
  {ansRep = Null;
   returnData = First[ToExpression[URLExecute[urlStem <> "CS_pack_" <> which]]];
   q = Style[TraditionalForm[ReleaseHold[returnData[[1]]]], TextAlignment -> Center];
   ans = ReleaseHold[returnData[[2]]];
   If[Not[ansRep === Null], ans = ansRep];
   mixed = TraditionalForm[#] & /@ ReleaseHold[returnData[[3]]];
   If[Length[returnData] == 3,
    pic = Null,
    pic = ReleaseHold[returnData[[4]]];
    If[Not[(Head[pic] === Graphics || Head[pic] === Image || Head[pic] === Graphics3D)], pic = Rasterize[TraditionalForm[pic], ImageSize -> {{480}, {540}}]]]};
(* end common functions *)

(* QUESTION AND ANSWERS FOR EACH CATEGORY *)
getQA[cat_] := {
   thisCat = cat;
   If[thisCat == "et Cetera", thisCat = "Humanity"];
   If[cat == "Grab Bag",
    qVal = Round[qVal * 1.5]; getQA[RandomChoice[cats]],
    randQ = RandomInteger[{1, 15}];
    short = If[StringLength[thisCat] > 3, StringTake[thisCat, 4], thisCat];
    qaFetch[short <> ToString[randQ]]],
   (* end Q&A *)

   (* FILL THE QA PAGE *)
   spacer2 = Pane[Null, {40, 0}],
   qValImg[1];
   If[pic === Null,
    body = Style[Column[{
        Pane[q, {1000, 172}, Alignment -> Center],
        Button[mixed[[1]], If[ans == 1, choseRt[], choseWr[]], Appearance -> "Frameless", Method -> "Queued", BaseStyle -> {FontFamily -> "Times New Roman", 32}],
        Button[mixed[[2]], If[ans == 2, choseRt[], choseWr[]], Appearance -> "Frameless", Method -> "Queued", BaseStyle -> {FontFamily -> "Times New Roman", 32}],
        Button[mixed[[3]], If[ans == 3, choseRt[], choseWr[]], Appearance -> "Frameless", Method -> "Queued", BaseStyle -> {FontFamily -> "Times New Roman", 32}],
        Button[mixed[[4]], If[ans == 4, choseRt[], choseWr[]], Appearance -> "Frameless", Method -> "Queued", BaseStyle -> {FontFamily -> "Times New Roman", 32}]},
      Alignment -> Center, Spacings -> 1.1], FontFamily -> "Times New Roman", 32, LineIndent -> 0],
    If[ImageDimensions[pic][[1]] < 530,
     body = Style[Row[{Column[{
        Pane[q, {500, 200}, Alignment -> Center],
        Button[mixed[[1]], If[ans == 1, choseRt[], choseWr[]], Appearance -> "Frameless", BaseStyle -> {FontFamily -> "Times New Roman", 32}],
        Button[mixed[[2]], If[ans == 2, choseRt[], choseWr[]], Appearance -> "Frameless", BaseStyle -> {FontFamily -> "Times New Roman", 32}],
        Button[mixed[[3]], If[ans == 3, choseRt[], choseWr[]], Appearance -> "Frameless", BaseStyle -> {FontFamily -> "Times New Roman", 32}],
        Button[mixed[[4]], If[ans == 4, choseRt[], choseWr[]], Appearance -> "Frameless", BaseStyle -> {FontFamily -> "Times New Roman", 32}]},
      Alignment -> Center, Spacings -> 1.1], pic}], FontFamily -> "Times New Roman", 32, LineIndent -> 0],
     body = Style[Column[{Pane[q, {960, 84}, Alignment -> Center], pic, Grid[{{
        Pane[
           Button[mixed[[1]], If[ans == 1, choseRt[], choseWr[]], Appearance -> "Frameless", BaseStyle -> {FontFamily -> "Times New Roman", 32}], {460, If[MatchQ[mixed[[1]], ___[_Image | _Graphics]], 144, 100]}, Alignment -> Center],
        Pane[
           Button[mixed[[2]], If[ans == 2, choseRt[], choseWr[]], Appearance -> "Frameless", BaseStyle -> {FontFamily -> "Times New Roman", 32}], {460, If[MatchQ[mixed[[1]], ___[_Image | _Graphics]], 144, 100]}, Alignment -> Center]},
        {Pane[
           Button[mixed[[3]], If[ans == 3, choseRt[], choseWr[]], Appearance -> "Frameless", BaseStyle -> {FontFamily -> "Times New Roman", 32}], {460, If[MatchQ[mixed[[1]], ___[_Image | _Graphics]], 144, 100]}, Alignment -> Center],
        Pane[
           Button[mixed[[4]], If[ans == 4, choseRt[], choseWr[]], Appearance -> "Frameless", BaseStyle -> {FontFamily -> "Times New Roman", 32}], {460, If[MatchQ[mixed[[1]], ___[_Image | _Graphics]], 144, 100]},
       Alignment -> Center]}}]}, Alignment -> Center, Spacings -> 1.1], FontFamily -> "Times New Roman", 32, LineIndent -> 0]]]};

(* FUNCTIONS FOR GAME OPERATION *)
start[] := {
   score1 = score2 = 0;
   up = RandomInteger[{1, 2}];
   upΔ[];
   game = Column[{spacer1, banner, Dynamic[spacer2], Dynamic[body]}, Alignment -> Center, Background -> White];
   newQ[]};
newQ[] := {
   If[Abs[score1] >= 300 || Abs[score2] >= 300,
      body = victoryImg; scrDifImg[];
      If[score1 > score2, hand1 = Image[winnerPic, ImageSize -> 112]; hand2 = "", hand2 = Image[winnerPic, ImageSize -> 112]; hand1 = ""],
      try = 1;
      qVal = RandomChoice[Range[20, 96, 4]];
      qValImg[0];
      qValStr = Style["$" <> ToString[qVal], 96, Red, FontFamily -> "Times New Roman"];
      cats = RandomSample[{"Numbers", "Algebra", "Geometry", "Graphs", "History", "Art", "Biology", "Literature", "Physics", "Language", "Astronomy", "Chemistry", "Geography", "Anatomy", "et Cetera"}, 9];
    catsLt = 
     Column[Button[Dynamic[#], getQA[#[[1]]], 
         Appearance -> "Frameless", Method -> "Queued"] & /@ 
       Take[Style[#, 48, FontFamily -> "Times New Roman"] & /@ cats, 
        5], Alignment -> Center, Spacings -> 4];
    catsRt = 
     Column[Button[Dynamic[#], getQA[#[[1]]], 
         Appearance -> "Frameless", Method -> "Queued"] & /@ 
       Append[Take[
         Style[#, 48, FontFamily -> "Times New Roman"] & /@ cats, -4],
         grabBag], Alignment -> Center, Spacings -> 4];
    body = Row[{catsLt, Pane[Null, 200], catsRt}]]};
choseRt[] := {
   EmitSound[happySound];
   If[up == 1, score1 += qVal, score2 += qVal];
   If[try == 1, up\[CapitalDelta][]];
   newQ[]};
choseWr[] := {
  EmitSound[sadSound];
  If[up == 1, score1 -= qVal, score2 -= qVal];
  If[try == 1,
   qVal *= .5; qValImg[1]; up\[CapitalDelta][]; try = 2,
   newQ[]]}
upΔ[] := {
   If[up == 1,
    up = 2; hand1 = ""; 
    hand2 = Image[hand2Pic, ImageSize -> {112, 58}],
    up = 1; hand2 = ""; 
    hand1 = Image[hand1Pic, ImageSize -> {112, 58}]]};
qValImg[g_] := {
   catDisp = If[thisCat == "Humanity", "et Cetera", thisCat];
   qValStr = 
    Style[StringReplace["$" <> ToString[qVal], "." -> ""], 96, Red, 
     FontFamily -> "Times New Roman"];
   catStr = 
    Style[If[g == 1, catDisp, "choose"], 30, 
     FontFamily -> "Times New Roman"];
   valCat = 
    ImageCompose[
     ImagePad[Rasterize[qValStr], {{0, 0}, {10, 10}}, White], 
     Rasterize[catStr, Background -> None], {Center, Bottom}, {Center,
       Bottom}];
   valueBox = 
    ImageMultiply[GaussianFilter[ColorNegate[EdgeDetect[valCat]], 5], 
     valCat]};
scrDifImg[] := {
   scrDifStr = 
    Style[StringReplace["$" <> ToString[Abs[score1 - score2]], 
      "." -> ""], 96, Red, FontFamily -> "Times New Roman"];
   difStr = Style["difference", 30, FontFamily -> "Times New Roman"];
   scoreDif = 
    ImageCompose[
     ImagePad[Rasterize[scrDifStr], {{0, 0}, {10, 10}}, White], 
     Rasterize[difStr, Background -> None], {Center, Bottom}, {Center,
       Bottom}];
   valueBox = 
    ImageMultiply[
     GaussianFilter[ColorNegate[EdgeDetect[scoreDif]], 5], scoreDif]};

(* INITIALIZE AND FILL THE CATEGORY PAGE *)

grabBag = Style["Grab Bag", 48, FontFamily -> "Times New Roman"];
scoreBox1 = 
  Style[Column[{"Team 1", 
     Dynamic[StringForm["``$``", If[score1 < 0, "-", ""], 
       Round[Abs[score1]]]]}, Alignment -> Center], 48, 
   FontFamily -> "Times New Roman"];
scoreBox2 = 
  Style[Column[{"Team 2", 
     Dynamic[StringForm["``$``", If[score2 < 0, "-", ""], 
       Round[Abs[score2]]]]}, Alignment -> Center], 48, 
   FontFamily -> "Times New Roman"];
spacer1 = Pane[Null, {40, 16}];
banner = Row[
   {Pane[Dynamic[scoreBox1], 197, Alignment -> Center],
    Pane[Dynamic[hand1], 180, Alignment -> Center],
    Pane[Dynamic[valueBox], 198, Alignment -> Center],
    Pane[Dynamic[hand2], 180, Alignment -> Center],
    Pane[Dynamic[scoreBox2], 197, Alignment -> Center]}, 
   Alignment -> Center];
spacer2 = Pane[Null, {40, 40}];
happySound = 
  Sound[{SoundNote["D", .15, "Crystal"], 
    SoundNote["A", .1, "Crystal"], SoundNote["D5", .1, "Crystal"], 
    SoundNote["F#5", .15, "Crystal"], SoundNote["D5", .1, "Crystal"], 
    SoundNote["A5", 1, "Crystal"]}];
sadSound = 
  Sound[{SoundNote["AFlat3", .125, "Woodblock"], 
    SoundNote["BFlat3", .1, "Woodblock"], 
    SoundNote["LowFloorTom", 2]}];
victoryImg = Image[victorPic, ImageSize -> 1000];
newGame[] := {
   game = 
    Button[Image[coverPic, ImageSize -> {1000, 720}], start[], 
     Appearance -> "Frameless"];
   Column[{Framed[
      Panel[Dynamic[game], Alignment -> {Center, Top}, 
       ImageSize -> {1000, 730}, Background -> White, 
       FrameMargins -> None]],
     Row[{
       Button["New Game", newGame[], ImageSize -> 168],
       Button["Back", Beep[], ImageSize -> 168],
       Button["New Question", getQA[thisCat], ImageSize -> 168],
       Button["Switch Team", up\[CapitalDelta][], ImageSize -> 168],
       Button["Score 1", Beep[], ImageSize -> 168],
       Button["Score 2", Beep[], ImageSize -> 168]}]}]};
newGame[]
POSTED BY: Mark Greenberg
Answer
1 month ago

enter image description here - Congratulations! This post is now a Staff Pick as distinguished on your profile! Thank you for your wonderful contributions. Please, keep them coming!

POSTED BY: Moderation Team
Answer
16 days ago

Group Abstract Group Abstract