Showing posts with label games. Show all posts
Showing posts with label games. Show all posts

Monday, April 29, 2019

Adventures In iPadding

Closest I'll get to the beach this week...
I recently replaced my home computer with an iPad. I can assure you that I’m as shocked about it as you are. I can’t remember the last time I didn’t have a home laptop, and was severely skeptical about this idea when I first started playing with it a couple of months ago.

What could have possessed me with such a lunatic notion?

This all started when I recognized two facts:

  1. The screen on my home machine (a MacBook Pro 13) was slightly icky. There’s a fairly well documented issue with the anti-reflective coating on them, but it didn’t actually affect me until after the warranty period expired. Beyond that, as I mentioned, it’s only slightly icky. The machine is completely usable, and in most cases I don’t notice anything about the screen at all. However, every now and then when I’m watching a YouTube video or sitting at just the right angle with a light behind me, I’d see little splotches of imperfection in the upper left corner or lower right of the display.
  2. The things for which I actually used my home computer could all (theoretically) be done on an iPad. I’ve mentioned YouTube already, and there’s an app for that. Social media? Apps for those too. Internet browsing? Yup, apps exist. Writing and note taking? Again, apps. In fact, Scrivener (my go-to writing software) has a very well put together mobile app.
Even so, I was hesitant. It’s not like I needed a new home machine. I’ve already admitted that the current MBP13 was fine-ish — certainly fully functional. More machine than I needed, actually, as I’d moved to doing personal project programming on my work* machine.

Work* machine: a new machine for which the company paid half and I paid half but I actually own. That deal actually worked out really well for me.

Then, two other things happened.
  1. I watched PubDraw and subsequently decided to pick up drawing as a new hobby. The intent was to draw on paper (and still is, to a large degree). But... if I had an iPad, I could draw using the Apple Pencil and stuff! It was an option, at least, that wasn’t surfaced in any meaningful way on my home computer.
  2. Gabriel’s computer went kaput. Not just a little kaput, either. The SSD can’t be mounted, which most likely means that either there’s a legitimate hard drive failure or a motherboard issue. Either one is going to be pretty pricey to fix, and Gabriel uses his computer quite a bit for school stuff. Having to wait for a repair wouldn’t be the end of the world, but having a replacement computer ready to go would be better, if possible.
The decision was made: I would get an iPad.

So what’s it like?

I wound up getting an iPad Pro 11 with 256GB of storage and WiFi only. When I got it, it was priced at just over $900. I also got the Apple Pencil, but I did NOT get the default iPad keyboard. For one thing, it’s expensive (almost $200). For another, it wasn’t available when I placed my order. Instead, I got an IVSO keyboard, which has been really good so far. It’s backlit (although that drains its battery pretty quickly), and the keyboard itself has well-made keys that provide satisfying but not overly annoying clicks. It has a place to store the pencil and allows for recharging it without having to take the case off. And it provides a little protection as a folio, although I wouldn’t suggest throwing it on the ground to test the durability.

The Goods


Apps for everything!
As previously mentioned, there’s an app for everything I’ve wanted and needed to do. I’m most pleasantly surprised by the Microsoft Office apps. Outlook wasn’t a big surprise, as I’ve used it on my iPhone for several years now. Excel, on the other hand, is just fantastic. It works exactly as you expect it to. Word is also pretty good, although not perfect.

Good experience doing my normal things
In the first week of hard usage, I put the iPad through the paces as well as I could. The biggest test for me was Transformers Night, where I participate in a tabletop role-playing game set in the Transformers universe (from the old cartoon show, not the recent crap). The main apps I use are Evernote, Excel, and Chrome. All of that worked really well, although the Evernote app, just like most iOS apps, takes some getting used to. 

I also used Scrivener pretty extensively in preparation for our D&D game (yes, I’m involved in more than one tabletop role-playing game. You want in on this action?). The app itself is full featured and fantastic, although I missed the ability to get between two docs in different parts of the tree with a single click.

Drawing!
I’ve already mentioned this a little, but drawing on the iPad is actually quite a lot of fun. Adobe has a free version of their Illustrator app (called Adobe Draw) that provides more than I can use right now. And before you ask: yes, I’m still terrible at it. That’s ok though. I’ll improve with research and practice.

The Apple Pencil is pretty fun as well. It works exactly as advertised, pairing seamlessly, responding to double-tapping, etc. The only critique I have of it is that it loses its charge pretty quickly. Luckily, it regains it even more quickly, so it hasn’t been an issue thus far.

Games
There are at least two games that I love that aren’t really playable on the iPhone but are nearly divine on the iPad: Terraria and Final Fantasy VII. Both are complete time-sinks and totally worth it. There are also a collection of OTHER games that I used to play on my old iPad that I’d forgotten about. Dang it, now I need to find more time for games...

Miscellany
Watching videos on the iPad is good in and of itself, but I was surprised at how often Tanya and I watch clips from Stephen Colbert or Seth Meyers in bed. Doing so on this as opposed to the iPhone is very satisfying, despite the higher required dexterity. Depending on usage, I only charge the iPad once or twice a week, which is a marked difference from the computer (which I left plugged in almost all the time for some reason).

The Bads


Accessing files on cloud storage
By default, the iPad doesn’t allow you to easily access any cloud storage device except iCloud. This is unfortunate, since most of our shared files are either on a Google drive or Dropbox. Installing both of those apps helps, but still doesn’t make it easy to maintain files stored in those media. Some apps are better than others with integration, which is a life-saver. Scrivener, for example, has a Dropbox-backed location built into the app.

I understand why Apple made this decision, but it’s pretty frustrating nonetheless.

Fonts and printing
You can’t just download and install fonts from the Internet on the iPad. This was a surprise to me, the fact that there’s no default (or at least, accessible) font manager. I found a free app that seems to do the job, but it’s not very straightforward. Even after downloading and installing the font, not every app (I’m looking at you, Google Docs) will let you access them.

Printing is also slightly challenging. The convention on the iPad takes some getting used to. You have to “share” whatever it is you want to print, and then navigate the Sharing dialog until you get to the printer selection. The good news is that lots of printers support AirPrint (including ours), so the main challenge really is finding the printer selection. And, as with so many things on iPad, how you get to that area is completely up to the app.

Copy/Paste
Most apps don’t support the notion of “Ctrl-C/Ctrl-V” for copying and pasting, which is taking quite a bit of getting used to. Some apps (I’m looking at you, Microsoft Office) have special icons for copying/pasting, which is something you have to be aware of. Most of the time, using the screen’s touch-centric copy/paste features works, but becoming adept at it will continue to be challenging for me, I suspect.

App Store
Let’s face it — the App Store is simply not as nice as being able to install something from a website or from the command line. And there are quite a few things that work perfectly well in the browser on a computer that don’t work very well in the browser on the iPad (I’m looking at you, Blogger). A lot of these things have iOS apps, but they also tend to cost money. Again, annoying, as my general rule has been: if you can’t get it for free on iOS, it might not be worth getting. And yes, I know this is a ridiculous rule, and I don't even follow it, but for some reason still find myself hesitating to shell out $0.99 for an app that I will use daily for the rest of my life... I'm weird.

Verdict

Despite the Bads list above, as of this writing, I have to rate my experience with the iPad quite highly. I don’t feel like I’ve lost anything irreplaceable during the switchover. Gabriel inherited a machine that will hopefully last him a while and that he’ll leverage more heavily than I was. I spent a fair chunk of money, but not as much as I would have for an equivalent MacBook Air. I'm genuinely enjoying messing around on the iPad, and am very glad I got it!

Monday, September 4, 2017

Your Random May Vary


At the beginning of the summer, we started hosting a semi-monthly Dungeons and Dragons game at our house. It's typically a lot of fun, involving much role-playing and dice rolling. We share Dungeon-Mastering duties; I think I've run most of the sessions, but at least two or three have been run by someone else. When I'm running the game, my character is typically an NPC. It's a bit of a shame, because my stats are pretty awesome.

I got to thinking this morning... I rolled my current character (as all my characters), using real dice and a "roll-4-keep-3" approach. I roll four 6-sided dice, discarding the lowest, and summing the other three. I do that six times in a row, and I've got the stats block for a new character.

I never use anything but real dice unless I happen not to have a set (and fyi: I carry a couple of sets around in my backpack just in case a spontaneous or random one-shot happens). Garrett used to roll great stats very often using his IPhone dice, and tended to roll challenges really highly as well. I started thinking about the difference in our results and decided to mess around with a rolling app this morning.

It's been a while since I posted anything technical. Bear with me.

I started by creating a little app that would roll a six-sided dice for me.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(ns stats-generator.main
  (:gen-class))

(def d6 [1 2 3 4 5 6])

(defn roll-d6 []
  (rand-nth d6))

(defn -main
  "I don't do a whole lot ... yet."
  [& args]
  (println "Hello, World!"))

I quickly realized that I didn't quite know where to go. TDD to the rescue!

1
2
3
4
5
6
(ns stats-generator.main-test
  (:require [clojure.test :refer :all]
            [stats-generator.main :as main]))

(deftest test-roll-4-keep-3
  (testing "Given a roll of 4 6-sided dice, sums the highest 3"))

I started with a description of one of the end goals: a function that would roll four dice and keep the highest three. That... is probably too broad. Let's decompose for a moment. We know we'll need to roll four 6-sided dice and drop the lowest roll. Maybe that dropping function is easier to test and implement.

1
2
3
4
5
(deftest test-drop-lowest
  (testing "Given all different values, drops the lowest"
    (let [v [1 2 3 4]
          expected [2 3 4]]
      (is (= expected (main/drop-lowest v))))))

There's the test; we should stub a red (failing) function.

1
2
3
4
(defn drop-lowest
  "Given a vector, returns a vector sans its lowest value."
  [v]
  v)

Done! Run the tests!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(run-tests)

Testing stats-generator.main-test

FAIL in (test-drop-lowest) (main_test.clj:9)
Given all different values, drops the lowest
expected: [2 3 4]
  actual: [1 2 3 4]
    diff: - [2 3 4]
          + [1 2 3 4]

Ran 2 tests containing 1 assertions.
1 failures, 0 errors.
=> {:test 2, :pass 0, :fail 1, :error 0, :type :summary}

Failed as expected. What's a simple solution that will work?

1
2
3
4
(defn drop-lowest
  "Given a vector, returns a vector sans its lowest value."
  [v]
  (rest (sort v)))

Seems reasonable. Tests tell us...?

1
2
3
4
5
6
7
(run-tests)

Testing stats-generator.main-test

Ran 2 tests containing 1 assertions.
0 failures, 0 errors.
=> {:test 2, :pass 1, :fail 0, :error 0, :type :summary}

Yay! Green tests! Let's fill in some other assertions to make sure we're not fooling ourselves.

1
2
3
4
5
6
7
8
9
(deftest test-drop-lowest
  (testing "Given all different values in random order, drops the lowest"
    (is (= [2 3 4] (main/drop-lowest [1 2 3 4])))
    (is (= [2 3 4] (main/drop-lowest [4 2 3 1]))))
  (testing "Given identical values, drops one of them"
    (is (= [1 1 1] (main/drop-lowest [1 1 1 1]))))
  (testing "Given some duplicated values, drops whatever's lowest"
    (is (= [2 2 3] (main/drop-lowest [2 3 2 2])))
    (is (= [5 6 6] (main/drop-lowest [5 6 4 6])))))

That's good enough. A question I'm often asked is "how much automated test coverage should I implement?" The pedantic answer is "100%," but the pragmatic answer is "as much as you can that covers stuff you would do by hand anyway." That's my rule-of-thumb, in any case. Sometimes I do more, sometimes less. But if I find bugs, I try to reproduce them with tests and then they're covered into perpetuity. As such the test coverage grows organically.

Alright, let's fill in some more of the decomposed functions and see where we wind up.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
(ns stats-generator.main
  (:gen-class))

(def d6 [1 2 3 4 5 6])

(defn roll-d6 [_]
  (rand-nth d6))

(defn roll-4
  "Rolls a d6 4 times"
  []
  (map roll-d6 (range 4)))

(defn drop-lowest
  "Given a vector, returns a vector sans its lowest value."
  [v]
  (rest (sort v)))

(defn sum [v]
  (apply + v))

(defn roll-4-keep-3
  "Rolls a 6-sided die 4 times, summing the highest 3 values"
  []
  (-> (roll-4)
      (drop-lowest)
      (sum)))

(defn -main
  "I don't do a whole lot ... yet."
  [& args]
  (println "Hello, World!"))

That seems reasonable. Test coverage is minimal, but enough for me to know that I'm not doing something overtly silly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
(ns stats-generator.main-test
  (:require [clojure.test :refer :all]
            [stats-generator.main :as main]))

(deftest test-roll-4
  (let [roll (main/roll-4)]
    (is (= 4 (count roll)))
    (is (every? #(<= 1 % 6) roll))))

(deftest test-drop-lowest
  (testing "Given all different values in random order, drops the lowest"
    (is (= [2 3 4] (main/drop-lowest [1 2 3 4])))
    (is (= [2 3 4] (main/drop-lowest [4 2 3 1]))))
  (testing "Given identical values, drops one of them"
    (is (= [1 1 1] (main/drop-lowest [1 1 1 1]))))
  (testing "Given some duplicated values, drops whatever's lowest"
    (is (= [2 2 3] (main/drop-lowest [2 3 2 2])))
    (is (= [5 6 6] (main/drop-lowest [5 6 4 6])))))

(deftest test-sum
  (is (= 18 (main/sum [6 6 6])))
  (is (= 3 (main/sum [1 1 1])))
  (is (= 12 (main/sum [3 4 5]))))

(deftest test-roll-4-keep-3
  (testing "Given a roll of 4 6-sided dice, sums the highest 3"
    (is (<= 3 (main/roll-4-keep-3) 18))))

At this point, I'm ready to do the REAL implementation. We'll set up a little recursion to see if we can generate a set of six 18s. I'm not good at math, but the likelihood seems pretty low that I'll actually do it. Let's compromise and try 100 MILLION times. We'll also squawk if we get close.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
(defn roll-all-18s []
 (let [counter (atom 0)]
   (println "STARTED AT:" (now))
    (loop [current-roll []]
      (swap! counter inc)
      (cond
        (= 100000000 @counter)
        (do
          (println "Gave up after ONE HUNDRED MILLION sets...")
          (println "ENDED AT:" (now)))

        (= [18 18 18 18 18 18] current-roll)
        (do
          (println "All 18s took" @counter "attempt(s).")
          (println "ENDED AT:" (now)))

        :default
        (do
          (when (<= 5 (count (filter #(= 18 %) current-roll)))
            (println "close call!" current-roll))
          (recur (roll-stats)))))))

Alrighty! We can run this in the REPL. The first try's results follow.

1
2
3
4
5
(roll-all-18s)
STARTED AT: #inst "2017-09-04T17:26:34.981-00:00"
close call! (5 18 18 18 18 18)
Gave up after ONE HUNDRED MILLION sets...
ENDED AT: #inst "2017-09-04T17:44:07.983-00:00"

It took us 22 minutes to try 100 MILLION times, and in all those tries, we only got close once. The frequency lines up with my (admittedly bad-at-math-and-fuzzy) expectations.

So how in the world did Garrett's IPhone roll so well for him so often? I suspect they weren't using the same approach as I was (roll-4-keep-3). After a little more research, I found that another approach that simply took a random integer between 3 and 18. This seemed like a pretty simple and probably more efficient implementation, but I suspected that the level of randomness in that picking was LOWER than that of the roll-4-take-3 approach. Again, I don't have the math or computer science to tell you why -- it was just my instinct.

So, I implemented a couple more rolling functions...

1
2
3
4
(def range-3-18 (range 3 19))

(defn roll-d3-18 [_]
  (rand-nth range-3-18))

...and refactored the roll-stats function into a multi-method that dispatches on the roll style you wanted to use.

1
2
3
4
5
6
7
8
9
(defmulti roll-stats identity)

(defmethod roll-stats :roll-4-keep-3
  [_]
  (map roll-4-keep-3 (range 6)))

(defmethod roll-stats :range-3-to-18
  [_]
  (map roll-d3-18 (range 6)))

No tests were added. I wanted to get to the bottom of this silly thing. Let's do some more REPLing!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
(roll-all-18s :range-3-to-18)
STARTED AT: #inst "2017-09-04T21:56:32.650-00:00"
close call! (18 12 18 18 18 18)
close call! (18 18 17 18 18 18)
close call! (18 18 18 18 18 10)
close call! (18 18 18 18 17 18)
close call! (18 18 18 18 12 18)
close call! (18 18 18 18 18 7)
close call! (18 18 18 9 18 18)
close call! (18 18 18 17 18 18)
close call! (18 18 18 18 8 18)
close call! (18 13 18 18 18 18)
close call! (18 9 18 18 18 18)
close call! (18 18 14 18 18 18)
close call! (18 18 18 4 18 18)
close call! (18 18 18 18 12 18)
close call! (18 18 18 18 10 18)
close call! (18 18 18 18 18 3)
close call! (18 3 18 18 18 18)
close call! (18 18 18 18 10 18)
close call! (18 18 10 18 18 18)
close call! (18 18 18 18 18 3)
close call! (18 18 18 18 15 18)
close call! (18 18 15 18 18 18)
close call! (18 18 18 18 12 18)
close call! (18 18 18 11 18 18)
close call! (3 18 18 18 18 18)
close call! (18 18 15 18 18 18)
close call! (18 18 18 9 18 18)
close call! (18 18 18 18 18 7)
close call! (17 18 18 18 18 18)
close call! (18 18 18 18 15 18)
close call! (18 18 18 18 11 18)
close call! (18 17 18 18 18 18)
close call! (18 4 18 18 18 18)
close call! (18 18 16 18 18 18)
close call! (18 18 18 8 18 18)
close call! (18 18 18 18 9 18)
close call! (18 18 15 18 18 18)
close call! (9 18 18 18 18 18)
close call! (18 18 18 18 7 18)
close call! (18 11 18 18 18 18)
close call! (18 18 18 6 18 18)
close call! (18 18 18 18 8 18)
close call! (18 18 18 15 18 18)
close call! (18 18 18 18 18 9)
close call! (18 18 9 18 18 18)
close call! (18 18 18 18 4 18)
close call! (18 18 18 18 11 18)
close call! (7 18 18 18 18 18)
close call! (18 18 18 18 18 3)
close call! (18 11 18 18 18 18)
close call! (18 18 12 18 18 18)
close call! (18 18 18 5 18 18)
close call! (18 18 18 18 18 6)
close call! (18 17 18 18 18 18)
close call! (18 6 18 18 18 18)
close call! (18 18 18 9 18 18)
close call! (18 18 18 11 18 18)
close call! (18 11 18 18 18 18)
close call! (18 16 18 18 18 18)
close call! (14 18 18 18 18 18)
close call! (18 18 10 18 18 18)
close call! (18 18 18 18 18 12)
close call! (18 18 18 18 18 16)
close call! (18 18 18 4 18 18)
close call! (18 18 18 18 12 18)
close call! (18 11 18 18 18 18)
close call! (18 18 18 18 16 18)
close call! (18 18 5 18 18 18)
close call! (18 18 18 18 18 16)
close call! (18 18 18 17 18 18)
close call! (5 18 18 18 18 18)
close call! (18 18 18 11 18 18)
close call! (18 18 18 18 18 12)
close call! (18 18 18 18 18 7)
close call! (18 18 18 3 18 18)
close call! (18 18 18 18 8 18)
close call! (18 18 18 5 18 18)
close call! (15 18 18 18 18 18)
close call! (18 5 18 18 18 18)
close call! (18 18 18 16 18 18)
close call! (18 18 18 18 4 18)
close call! (18 18 17 18 18 18)
close call! (18 14 18 18 18 18)
close call! (18 18 18 18 18 7)
close call! (18 18 18 4 18 18)
close call! (18 18 18 18 4 18)
All 18s took 13880334 attempt(s).
ENDED AT: #inst "2017-09-04T21:57:12.554-00:00"

WHOA! There were a TON more close calls, and an actual hit about 14 milllion rolls in.

I decided to re-run the roll-4-take-3 strategy to verify its performance.

1
2
3
4
5
6
7
(roll-all-18s :roll-4-keep-3)
STARTED AT: #inst "2017-09-04T22:51:37.468-00:00"
close call! (18 18 18 18 14 18)
close call! (13 18 18 18 18 18)
close call! (18 18 18 18 18 9)
Gave up after ONE HUNDRED MILLION sets...
ENDED AT: #inst "2017-09-04T23:09:02.571-00:00"

A few more close calls were had, but roughly the same result. I re-ran the range-3-to-18 strategy:

1
2
3
4
5
6
(roll-all-18s :range-3-to-18)
STARTED AT: #inst "2017-09-04T23:24:08.765-00:00"
close call! (18 18 8 18 18 18)
close call! (18 18 18 5 18 18)
All 18s took 321349 attempt(s).
ENDED AT: #inst "2017-09-04T23:24:09.925-00:00"

Wowzers. SO MUCH MORE LIKELY to roll 18s using that approach. The second run only took a second and slightly more than 321,000 tries.

So, what's the moral of the story? I think it's that the amount of randomness you get from computers may vary, based on what the underlying strategy is. Random numbers aren't truly random in the machine. Always keep that in mind when they rise up and become our overlords.

Also: rolling 321,000 times by hand, at one set of rolls per second, would still take you almost 4 straight days of rolling to get that all 18s set. So for you DMs out there... if a player comes to you with a character they said they rolled, and it's got six 18s, you give them a knowing wink while handing them your dice and telling them "roll again..."

Tuesday, August 9, 2016

Summer Vacacay p3: Interlude 1

Beach house!
The forecast for today said it was going to be cloudy all day with a good chance of rain. Combine that with some slight muscle soreness from yesterday, and we decided it would be wise to take it kind of easy today.  As such, we opted to stroll up and down the beach a bit.

This was the first time in our two-and-a-half days that we actually made it down to the beach, and it was pretty nice.  The breeze coming in off of the Pacific was refreshing, even invigorating.  The sand itself was very fine near the foliage, but fairly packed nearer the water, making the strolling pretty easy once we got away from the house.

Turning right, we made our way northward, keeping the ocean on our left and the treeline on our right.  There were a few spots where the surf was still running up the beach pretty well, making our trip almost adventurous.
Graceful as a lame gazelle!

Luckily for me, the boys were there to keep me grounded.

Srsly dad... are we there yet?
Dad, we need to talk about all this walking...

The sky gave us glimpses of blue, so we had hopes that there would be some sunshine today.

Tide pools and blue sky!

Unfortunately, that was the most blue we saw today.  Shortly after we got back to the house, it started sprinkling, and turned into an almost-honest rain.

The only solution was a board game.  We had purchased a game called "Cash n' Guns" a couple of weeks ago but hadn't had a chance to play it.  The boys thought it would be a good idea to pack it with us.  Their forethought gave us a pretty fun half-hour.

Nobody saw nothin', see?
It's a game where each player takes on the role of a robber, and they've (collectively) scored a huge heist.  The robbers go through the process of splitting up the loot over 8 rounds, only they manage to pull guns on each other every round.  You get to choose whether or not you actually shoot or just fake shoot someone every round, except that you *have* to shoot at least 3 of 8 rounds.  You can also choose whether or not to dodge the potential gunshots, in which case you can't grab any loot that round.  People that don't get shot and don't dodge get to share the loot.  The person with the most loot after 8 rounds wins.  Any guesses on the final scores?  Or at least who won?

If you guessed Tanya, then Gabriel slightly edging out Garrett, with me bringing up the rear in a distant 4th, then you should buy some lottery tickets tonight as well. :-)

Tanya and I wanted to have dinner in town at another well-rated seafood place.  However, neither boy could be convinced to join us, so... Mac 'n Cheese round 2 for them!  Meanwhile, Tanya and I went to Local Ocean Seafoods in Newport.  Tanya had the Fishwives Stew, and I had the Seared King Salmon.  Both were very good, but most importantly, we had two desserts: a homemade Lemon Cookie Ice Cream and a Shortbread Parfait.  If you are in the area and *don't* try the Lemon Cookie Ice Cream, YOU ARE A FOOL!  That ice cream makes me want to buy an ice cream maker and grow a Meyer lemon tree.

ZOMG!  GET IN MAH BELLY!!
The other thing I spent a bunch of time doing today was trying to wrap my head around the Firefly RPG rules so that the boys and I can do an Episode sometime this week.  Nerdy adventures await, people!  However, tomorrow we'll be back to the active schedule, so you'll probably have to wait until Thursday for outer space adventures...




Sunday, March 20, 2016

The Big Short

A short (no pun intended) offering today, as I'm currently intellectually and emotionally exhausted from worrying about the future of our country and the world in general...

Last night, Tanya and Marco and Tanya and I (yes, two separate Tanya's) watched The Big Short, the movie about the guys that made a ton of money off of betting against the American economy from 2005 through 2008 or so.  Well, that's not really what the movie (or book) is about.  The story actually follows and exposes the absolutely ridiculous series of unfortunate events that got us to the point of the housing market bubble bursting.  The fact that those people that bet against the market made a lot of money in the meantime is almost a footnote, which I thought was really interesting.

The movie was poignant and entertaining, which is also a pretty rare combination.  Every performance was memorable and well executed, with the exception of Brad Pitt -- sorry, he's really hit-or-miss with me and felt very average in this role.  There was a greater than usual amount of breaking the fourth wall, and it was always compelling and appropriate.

In light of our current political climate, I encourage everyone that isn't familiar with the story here to go watch the movie and/or read the book.  Think about how the candidate you favor relates to (or is actually culpable in) the events portrayed there.  Consider how they might behave on a going-forward basis with regard to Wall Street (and fiscal responsibility overall).

Now bear in mind that this is only *one* of the really important issues that probably don't have enough substantive discourse in the debates from either party.  It is my sincere hope that everyone that can *will* participate in this year's political process and will do so thoughtfully.

Pick your top three issues.  Really think them through from as many perspectives as you can so that you can have a fair view of them.  Find the candidate that best aligns with those top three, and vett that they do so as much as you possibly can.  Don't just listen to what the candidate is saying -- look at their public/behavioral record.  This is relatively easy to do with current/former government officials, less so with private citizens.  Do your best in this effort so that you can support and vote with a clear conscience.

Insofar as the opportunities present themselves, participate in meaningful, thoughtful, and respectful discussion with people that disagree with the importance of your issues or your stances.  Be open to learning something, understanding someone else's point of view, and potentially changing your attitude.

Above all, be kind to yourself and to everyone else.  There's too much hate and misery in the world as it is -- don't contribute to vitriol or negative behavior/attitude.  I read this commentary yesterday and found that it resonated with my own mental state pretty well.  It was written more eloquently than I could have done, so please give it a read.

Alright, that's it for politics and sadness for me.  I promise the next post will be back to the normal book reviews, writing, exercise, technology, etc.  If you need a diversion until then, check out Tim's Snake Lake blog post and play the game.  Yum!!

Sunday, March 6, 2016

Cartagena, part 5

When last we spoke, we had managed to draw a board with spaces and icons.  Now it's time to fill in the players and finish up the web implementation.

Recall that we had left the server code in a state that if we called the new-game route without a :players keyword, we would default the new game to return a two-player game featuring tanya as orange and rusty as black.  I decided to work forward from here, because it let me defer the decision about how to capture the player data up front.  As it turns out, this was a great decision, because I'm terrible at user experience.  :-)

Calling the endpoint resulted in the server giving us a set of game data with two players. The players start in jail.  Now I needed to add the code that would actually display the players in that spot.

(defn jail []
  (when-let [jail (get-in @app-state [:board 0])]
    (apply concat
           (let [pirate-frequencies (frequencies (:pirates jail))
                 pirate-colors (vec (keys pirate-frequencies))]
             (for [player-index (range (count pirate-frequencies))]
               (let [pirate-color (get pirate-colors player-index)
                     pirate-count (pirate-color pirate-frequencies)
                     color-name (name pirate-color)
                     x (to-scale (+ 5 (* 10 player-index)))]
                 (for [pirate-index (range pirate-count)]
                   (let [y (to-scale (+ 35 (* 10 pirate-index)))]
                     [:circle
                      {:cx x
                       :cy y
                       :r (to-scale 4)
                       :fill color-name}]))))))))

I also needed to add this bit of rendering code to the main-view function, which was easy.

(defn main-view []
  [:center
   [:h1 "CARTAGENA"]
   [:div
    [:button
     {:class "btn btn-primary"
      :on-click (fn button-click [e]
                  (select-players!))}
     "New Game"]]

   [:div
    {:class "row"
     :style {:float "left" :margin-left 10 :margin-right 10}}
    [:div
     {:class "col-md-5"}
     (-> [:svg
          {:view-box (str "0 0 " (to-scale 501) " " (to-scale 301))
           :width (to-scale 501)
           :height (to-scale 301)}]
         (into (static-board))
         (into (jail))
         )]]])

The result was marvelously exciting!

Alright, I might've oversold that a little.  But the fact of the matter is, I had a set of players on the board, in the right position!  SO CLOSE TO BEING FINISHED!

...except that I really wasn't.  How in the world was I going to handle moving the pieces around?  Recall that the rules state that you can take three actions on a turn: playing a card to move a pirate forward, selecting a pirate to move backward in order to draw cards, and passing.  At this point, I can't play any cards because I can't tell who all has what.  I can't move backward, because there's nowhere to move backward to.  I can't even pass, because there's no action button or keystroke that will allow me to do that.

It looks like I'm going to have to break down and build a player area.  <insert ominous music here>

As I've already stated, I don't consider myself to be much of a user experience expert.  This became doubly apparent as I iterated through several different versions of the player area.  I finally landed on a design such that the player area is to the right of the game board.  Here's the code.  Notice that I baked it right into the main-view function, which makes that function needlessly cluttered.  I'm unapologetic.  It's not time to refactor yet...  :-D

(defn main-view []
  [:center
   [:h1 "CARTAGENA"]
   [:div
    [:button
     {:class "btn btn-primary"
      :on-click (fn button-click [e]
                  (select-players!))}
     "New Game"]]

   [:div
    {:class "row"
     :style {:float "left" :margin-left 10 :margin-right 10}}
    [:div
     {:class "col-md-5"}
     (-> [:svg
          {:view-box (str "0 0 " (to-scale 501) " " (to-scale 301))
           :width (to-scale 501)
           :height (to-scale 301)}]
         (into (static-board))
         (into (jail))
         )]]
   (when-let [active-player (active-player @app-state)]
     (let [{:keys [color cards] player-name :name} active-player
           card-groups (frequencies cards)]
       [:div
        {:class "col-md-4"}
        [:div {:style {:margin-right 10}}
         [:table {:class "table table-bordered table-responsive"}
          [:tbody
           [:tr
            [:td "Current Player"]
            [:td player-name]]]
          [:tr
           [:td "Color"]
           [:td
            [:svg
             {:width (to-scale 20)
              :height (to-scale 20)}
             [:circle
              {:cx (to-scale 10)
               :cy (to-scale 10)
               :r (to-scale 7)
               :fill (name color)}]]
            [:span {:style {:color (name color)}} (name color)]]]
          [:tr
           [:td "Actions Remaining"]
           [:td (:actions-remaining @app-state)]]
          [:tr
           [:td "Cards"]
           [:td (for [[card num] card-groups]
                  ^{:key card}
                  [:span {:style {:float "left"}}
                   [:figure
                    [:img
                     {:src (card icon-images)
                      :width (to-scale 30)
                      :height (to-scale 30)
                      :on-click (fn img-click [e]
                                  (select-card! card))}]
                    [:center [:figcaption num]]]])]]
          [:tr
           [:td "Selected Card"]
           [:td [:span {:style {:float "left"}}
                 (when-let [selected-card (:selected-card @app-state)]
                   [:img
                    {:src (selected-card icon-images)
                     :width (to-scale 30)
                     :height (to-scale 30)
                     :on-click (fn img-click [e]
                                 (unselect-card!))}])]]]
          [:tr
           [:td {:colSpan 2}
            [:center
             [:button
              {:class "btn"
               :on-click (fn btn-click [e]
                                   (update-active-player! @app-state))} "Pass"]]]]]]
        [:div
         [:p "To move forward, click a card, then click the target pirate.  To undo card selection, click the selected card."]
         [:p "To move backward, click the target pirate."]]]))])

This also required a couple of handler functions: select-card!, unselect-card! and update-active-player!  The update-active-player! function is actually a server request, since all of that logic is built into the game engine.

(defn update-active-player [state response]
  (assoc state :actions-remaining (:actions-remaining response)
               :current-player (:current-player response)))

(defn on-update-active-player [response]
  (swap! app-state update-active-player response))

(defn update-active-player! [{:keys [actions-remaining current-player player-order]}]
  (ajax/POST "http://localhost:3000/update-active-player"
             {:params {:actions-remaining actions-remaining
                       :current-player current-player
                       :player-order player-order}
              :handler on-update-active-player
              :error on-error}))

(defn select-card! [card]
  (reset! app-state (assoc @app-state :selected-card card)))

(defn unselect-card! []
  (reset! app-state (dissoc @app-state :selected-card)))

This set of code led to a usable UI, if not an aesthetically pleasing one.


Now we have the ability to choose cards (and unchoose them, if we accidentally clicked one) and pass, but we don't have the ability to choose a specific pirate to move forward or backward.  We also don't have any code to display pirates on any board space except the jail.  Time to add those bits to the appropriate spots.

;; TODO: this looks almost exactly like jail
(defn ship []
  (when-let [ship (get-in @app-state [:board 37])]
    (apply concat
           (let [pirate-frequencies (frequencies (:pirates ship))
                 pirate-colors (vec (keys pirate-frequencies))]
             (for [player-index (range (count pirate-frequencies))]
               (let [pirate-color (get pirate-colors player-index)
                     pirate-count (pirate-color pirate-frequencies)
                     color-name (name pirate-color)
                     x (to-scale (+ 445 (* 10 player-index)))]
                 (for [pirate-index (range pirate-count)]
                   (let [y (to-scale (+ 245 (* 10 pirate-index)))]
                     [:circle
                      {:cx x
                       :cy y
                       :r (to-scale 4)
                       :fill color-name
                       :on-click (fn ship-click [e]
                                   (pirate-click pirate-color 37))}]))))))))
                                   
(defn circles-for [space-index x y colors]
  (for [color-index (range (count colors))]
    (let [color (get colors color-index)
          color-name (name color)
          cx (to-scale (+ 35 x))
          cy (to-scale (+ y 5 (* 10 color-index)))]
      ^{:key color-index}
      [:circle
       {:cx cx
        :cy cy
        :r (to-scale 4)
        :fill color-name
        :on-click (fn circle-click [e]
                    (pirate-click color space-index))}])))

(defn normal-spaces []
  (apply concat
         (for [i (range 1 37)]
           (when-let [space-data (get-in @app-state [:board i])]
             (let [position (get piece-positions i)
                   x (:x position)
                   y (:y position)
                   space (normal-space x y)
                   image (space-image x y (:icon space-data))
                   pirates (circles-for i x y (:pirates space-data))]
               (conj [space image] pirates))))))

Notice the prolific pirate-click! function.  This is the function from whence the remaining server calls initiate.  It and its associated functions are a decent chunk of code, but it all feels very similar to the code I did for the console version.

;; TODO: figure out a way to make this work elegantly with callbacks
;; swiped from server code
(defn game-over?
  "Returns truthy if a player has 6 pirates on the ship; otherwise nil."
  [board]
  (let [ship (first (filter #(= :ship (:icon %)) board))
        pirate-counts-by-color (frequencies (:pirates ship))]
    (some #(>= (second %) 6) pirate-counts-by-color)))

(defn end-game [state]
  (assoc state :game-over true))

(defn end-game! []
  (swap! app-state end-game))

(defn play-card [state response]
  (-> state
      (assoc :board (:board response)
             :discard-pile (:discard-pile response)
             :players (conj (remove #{(active-player state)} (:players state)) (:player response)))
      (dissoc state :selected-card)))

(defn on-play-card [response]
  (let [board (:board response)]
    (swap! app-state play-card response)
    (if (game-over? board)
      (end-game!)
      (update-active-player! @app-state))))

(defn play-card! [player card from-space board discard-pile]
  (ajax/POST "http://localhost:3000/play-card"
             {:params {:player player
                       :icon card
                       :from-space from-space
                       :board board
                       :discard-pile discard-pile}
              :handler on-play-card
              :error-handler on-error}))

(defn move-back [state board response]
  (assoc state :board board
         :draw-pile (:draw-pile response)
         :discard-pile (:discard-pile response)
         :players (conj (remove #{(active-player state)} (:players state)) (:player response))))

(defn on-move-back [response]
  (when-let [board (:board response)]
    (swap! app-state move-back board response)
    (update-active-player! @app-state)))

(defn move-back! [player from-space board draw-pile discard-pile]
  (ajax/POST "http://localhost:3000/move-back"
             {:params {:player player
                       :from-space from-space
                       :board board
                       :draw-pile draw-pile
                       :discard-pile discard-pile}
              :handler on-move-back
              :error-handler on-error}))

(defn pirate-click [color from-space-index]
  (when (= color (:color (active-player @app-state)))
    (let [player (active-player @app-state)
          board (:board @app-state)
          from-space (get board from-space-index)
          discard-pile (:discard-pile @app-state)]
      (if-let [selected-card (:selected-card @app-state)]
        (play-card! player selected-card from-space board discard-pile)
        (move-back! player from-space board (:draw-pile @app-state) discard-pile)))))

Now, FINALLY, I can click pirates and expect them to render properly.  Every click of a pirate calls the server, passing the current game state, and receiving an updated board from the engine.  I'm not completely satisfied with it, since I wound up duplicating some server code in the web implementation to check for the end game condition.  However, given the amount of typing I'd already done and the fact that I want to make it multi-player next, I decided it wasn't worth the effort to clean up.

All that's left is gathering the initial player data.  As you might've guessed, there was more than a little give-up in me at this point.  I decided the simplest thing that could work would be to create a silly little set of inputs, one for each color.  I won't bother showing you the hiccup for that, but the form looks like:


Are you impressed yet?!

As you might imagine, filling in names and pressing the Start button creates a players map that gets sent in the request to the new-game endpoint.  The server initializes a game state given the player data, and away we go!

So this is the end.  We've finally slogged through the initial web implementation for Cartagena.  If you've followed me thus far, you deserve a medal for your determination. I certainly learned a lot along the way, and hope that you have too (or at least that you were entertained by my bumbling through it).

I do intend to implement the multi-player non-local version of this at some point in the future, but it might be a while.  If you have a burning desire to see it done, let me know in the comments.  Otherwise, thanks for sharing my adventures through this series, and stay tuned for non-Cartagena posts coming your way soon!  :-D