1

I'm new to programming and have been learning Swift by doing a number of online courses. In one of the courses we built a basic trivia game and I've been progressively trying to improve it by doing my own coding (best way to learn!).

Recently I came across what's called a Fisher-Yates shuffle and, after much trial and error (and with the help of the stack overflow community) was able to use GKRandomSource from Swift's Gameplaykit to shuffle my trivia questions around so that they were being asked randomly. This was an improvement on the original arc4random code I was using because the shuffle removed questions already asked from the overall pool of questions, thereby ensuring that they did not repeat (at least in iOS9).

This works well within the session, but once the user quits the app and relaunches it, the shuffle starts from scratch. So I was looking at a way to have the app 'remember' the questions already asked between sessions. My research led me to the idea of seeding and I've been trying to get this to work with my GKRandomSource code, but I'm obviously missing something.

Any advice etc would be most welcome - especially since I'm not entirely sure that this 'seeding' approach will achieve my ultimate aim of not repeating questions already asked in previous sessions of the app.

Below are what I believe to be the relevant bits of my revised code.

All questions and potential answer choices are stored in a .json file as such:

{
        "id" : "1",
        "question": "Earth is a:",
             "answers": [
            "Planet",
            "Meteor",
            "Star",
            "Asteroid"
          ],
          "difficulty": "1"
      }

I use the following code to load the .json file:

func loadAllQuestionsAndAnswers()
{
    let path = NSBundle.mainBundle().pathForResource("content", ofType: "json")
    let jsonData : NSData = NSData(contentsOfFile: path!)!
    allEntries = (try! NSJSONSerialization.JSONObjectWithData(jsonData, options: NSJSONReadingOptions.MutableContainers)) as! NSArray
    //println(allEntries)

}

And below is my most recent code for trying to achieve the shuffle of all questions and replicate it in future sessions):

var allEntries : NSArray!
var shuffledQuestions: [AnyObject]!
var nextQuestion = -1


    var mySeededQuestions : [AnyObject]
    loadAllQuestionsAndAnswers()

    if #available(iOS 9.0, *) {
            let lcg = GKLinearCongruentialRandomSource(seed: mySeededQuestions)
            let shuffledQuestions = lcg.arrayByShufflingObjectsInArray(allEntries)
            nextQuestion++
            loadQuestion(nextQuestion)

            // Fallback on earlier versions

        }else{

            let randomNumber = Int(arc4random_uniform(UInt32(allEntries.count)))
            loadQuestionPreiOS9(randomNumber)

        }

I know at the very least I have a problem with the above code, but I'm at a loss. I'm also thinking that maybe I'm missing a step in terms of storing the seed?

For the sake of completeness, I use a label to display the question and four images to display the potential answers, using the following code:

func loadQuestion(index : Int)
{
    let entry : NSDictionary = shuffledQuestions[index] as! NSDictionary
    let question : NSString = entry.objectForKey("question") as! NSString
    let arr : NSMutableArray = entry.objectForKey("answers") as! NSMutableArray

    //println(question)
    //println(arr)

    labelQuestion.text = question as String

    let indices : [Int] = [0,1,2,3]
    //let newSequence = shuffle(indices)
    let newSequence = indices.shuffle()
    var i : Int = 0
    for(i = 0; i < newSequence.count; i++)
    {
        let index = newSequence[i]
        if(index == 0)
        {
            // we need to store the correct answer index
            currentCorrectAnswerIndex =  i

        }

        let answer = arr.objectAtIndex(index) as! NSString
        switch(i)
        {
        case 0:
            buttonA.setTitle(answer as String, forState: UIControlState.Normal)
            break;

        case 1:
            buttonB.setTitle(answer as String, forState: UIControlState.Normal)
            break;

        case 2:
            buttonC.setTitle(answer as String, forState: UIControlState.Normal)
            break;

        case 3:
            buttonD.setTitle(answer as String, forState: UIControlState.Normal)
            break;

        default:
            break;
        }



    }
    buttonNext.hidden = true
    // we will need to reset the buttons to reenable them
    ResetAnswerButtons()

}


func loadQuestionPreiOS9(index : Int)
{
    let entry : NSDictionary = allEntries.objectAtIndex(index) as! NSDictionary
    let question : NSString = entry.objectForKey("question") as! NSString
    let arr : NSMutableArray = entry.objectForKey("answers") as! NSMutableArray

    //println(question)
    //println(arr)

    labelQuestion.text = question as String

    let indices : [Int] = [0,1,2,3]
    //let newSequence = shuffle(indices)
    let newSequence = indices.shuffle()
    var i : Int = 0
    for(i = 0; i < newSequence.count; i++)
    {
        let index = newSequence[i]
        if(index == 0)
        {
            // we need to store the correct answer index
            currentCorrectAnswerIndex =  i

        }

        let answer = arr.objectAtIndex(index) as! NSString
        switch(i)
        {
        case 0:
            buttonA.setTitle(answer as String, forState: UIControlState.Normal)
            break;

        case 1:
            buttonB.setTitle(answer as String, forState: UIControlState.Normal)
            break;

        case 2:
            buttonC.setTitle(answer as String, forState: UIControlState.Normal)
            break;

        case 3:
            buttonD.setTitle(answer as String, forState: UIControlState.Normal)
            break;

        default:
            break;
        }



    }
    buttonNext.hidden = true
    // we will need to reset the buttons to reenable them
    ResetAnswerButtons()

}

Finally, I use the following code to present the user with a 'Next' button after they've answered a question:

@IBAction func PressedButtonNext(sender: UIButton) {
    print("button Next pressed")

    if #available(iOS 9.0, *) {
    nextQuestion++
    loadQuestion(nextQuestion)
    }else{
        let randomNumber = Int(arc4random_uniform(UInt32(allEntries.count)))
        loadQuestionPreiOS9(randomNumber)
    }

I know my coding is probably quite verbose and unnecessary, but up until this latest improvement it's been working fine and I actually understand most of it (I think!)

Monomeeth
  • 753
  • 3
  • 13
  • 29

2 Answers2

3

There are really two questions here: what you're asking about and what you seem to want. They're both worth answering for different reasons, so...

How to seed a GK(Whatever)RandomSource

(All the GKRandomSource subclasses have seeds, even though the superclass GKRandomSource itself doesn't... that's because each class has its own data type for seeds. But the usage is the same.)

The critical bits of the code you've posted don't even compile due to a type mismatch: the seed/init(seed:) value for GKLinearCongruentialRandomSource is an integer, not an array of objects. The documentation for that value spells out what it's for (emphasis added) and how to use it:

Any two random sources initialized with the same seed data will generate the same sequence of random numbers. To replicate the behavior of an existing GKLinearCongruentialRandomSource instance, read that instance’s seed property and then create a new instance by passing the resulting data to the initWithSeed: initializer.

So, if you want to replicate a sequence of random numbers:

  1. Create a random source with the plain initializer.

    let source = GKLinearCongruentialRandomSource()
    
  2. Save off that source's seed value.

    let seed = source.seed // -> some UInt64 value
    // write seed to user defaults, a file, a web service, whatever.
    
  3. Use that random source for whatever.

  4. Later, when you launch again and want the same sequence, read in the seed value and create a random source using the seed.

    let seed = // read in seed value from wherever you saved it
    let source = GKLinearCongruentialRandomSource(seed: seed)
    

This still doesn't get you what you're actually looking for, though: If source in step 1 produced the sequence 1, 6, 3, 9, 2, 7, source from step 4 will also produce the sequence 1, 6, 3, 9, 2, 7 — the seed doesn't record where you "left off" in a sequence. Or, since you're using it for an array shuffle, it'll produce the same shuffled ordering of the array as the first shuffle, but it doesn't remember what you did with the shuffled array thereafter.

How to use a shuffled ordering across multiple app launches

If you want to shuffle an array, walk through it in order, and then on a later run of your app continue walking through the same shuffled array from where you left off, you need to design around that requirement.

  1. Shuffle on the first launch.

  2. Record something about the ordering produced. (Say, a mapping of indices in the shuffle to indices in the original data.)

  3. When walking through the shuffled array, record how far you've gone through it.

  4. On later runs of the app, use the record of the ordering and the record of progress to decide where you are.

Here's a rough pass at that. (Note that I'm not touching your data model — this is a program design question, and SO is not a coding service. You'll need to think about how to flesh out this design to match your model and its use cases.)

struct Defaults {
    static let lastQuestionIndex = "lastQuestionIndex"
    static let questionOrder = "questionOrder"
}
let questions: [Question] // array of model objects, always in fixed order

func nextQuestion() -> Question {
    let defaults = NSUserDefaults.standardUserDefaults()
    if let lastIndex = defaults.integerForKey(Defaults.lastQuestionIndex) {
        // we've run before, load the ordering
        guard let shuffledOrder = defaults.arrayForKey(Defaults.questionOrder) as? [Int]
            else { fatalError("save questionOrder with lastQuestionIndex") }

        // advance the saved index so the next call to this function 
        // will get the next question
        if lastIndex + 1 < count {
            defaults.setInteger(lastIndex + 1, forKey: Defaults.lastQuestionIndex)
        } else {
            // ran out of shuffled questions, forget the order so we
            // can reshuffle on the next call
            defaults.removeObjectForKey(Defaults.questionOrder)
            defaults.removeObjectForKey(Defaults.lastQuestionIndex)
        }

        // map lastQuestionIndex from sequential to shuffled
        // and return the corresponding answer
        let shuffledIndex = shuffledOrder[lastIndex]
        return questions[shuffledIndex]

    } else {
        // first run, shuffle the question ordering (not the actual questions)
        let source = GKRandomSource()
        let sequentialOrder = Array(0..<questions.count)
        let shuffledOrder = source.arrayByShufflingObjectsInArray(sequentialOrder)

        // save the ordering, and the fact that we're asking the first question
        defaults.setObject(shuffledOrder, forKey: Defaults.questionOrder)
        defaults.setInteger(0, forKey: Defaults.lastQuestionIndex)

        // return the first question in the shuffled ordering
        let shuffledIndex = shuffledOrder[0]
        return questions[shuffledIndex]
     }
}

That's probably a bit pseudocode-ish (so you might need to worry about casting arrays to work with NSUserDefaults, etc), but as a general design it should be enough to give you some food for thought.

rickster
  • 124,678
  • 26
  • 272
  • 326
  • Thanks for the guidance. I did suspect I only had half of the potential solution to what I was wanting to achieve, and you've confirmed that! I'll take a look at this (hopefully today) and see if I can make sense of it all. :) – Monomeeth Feb 03 '16 at 21:42
  • So I'm looking at this now and was just wondering, since I effectively asked two questions and you've provided answers for both, do I need both to do what I wanted to do? That is, do I still need to seed a GKRandomSource to use a shuffled order across multiple app launches, or can I focus on your second answer for now to get me going again? – Monomeeth Feb 04 '16 at 09:42
  • The second part of this answer saves the order — the *output* of the random source — so it doesn't matter what you do to the random source itself. – rickster Feb 04 '16 at 14:47
  • I've been trying to adapt your design to my code but keep getting _Use of undeclared type ' '_ errors (even though I think I've declared them!). I also seem to get an _Initializer for conditional binding must have Optional type, not 'Int'_ error. When I fix these, I get errors in my other lines of code. So, is there a reference you'd recommend I consult that's written for a beginner such as myself? I've done Google searches, referred to the Swift Programming Language Guide, and read forum discussions, but they all assume a higher level of knowledge. – Monomeeth Feb 05 '16 at 00:57
0

You can also use the following to drop off a certain amount of values so if you keep a roll count dropping that many on next start is as easy as :

arc4.dropValues(rollCount)
leerie simpson
  • 186
  • 1
  • 3