August 25th, 2017

The Sneaky Snacky Squirrel

Previously I wrote about a board game our daughter enjoys playing, Hi Ho! Cherry-O. One of the things that makes it somewhat uninteresting to adults is the lack of any applicable strategy; luck completely determines the outcome. Another game she enjoys is The Sneaky Snacky Squirrel. While luck determines much of the outcome, there are also two opportunities to strategize, when the result of a spin allows one to:

  1. Pick 1 or 2 acorns of needed color(s).
  2. Steal an acorn of a needed color from another player.

Our daughter understands what colors she needs to pick but not that there might be a better strategy than just picking her favorite color or always picking from mama’s log. When playing with her it’s also difficult to test if a non-naive strategy actually matters since she doesn’t appreciate it when people take acorns from her (unless there’s no other option). It’s also difficult to round up other adults to play a bunch of rounds of the game…so once again a simulator is the best option.

For the picking strategy, instead of just picking any color acorn(s) needed, it might be more advantageous to pick the color(s) that the most people already have. That way if another player has an opportunity to steal, it’s less likely they’ll be able to steal from you.

For the stealing strategy, instead of just picking any color acorn needed from any player, it again might be more advantageous to pick the color that the most people have, from the player with the most acorns. That way again if another player has an opportunity to steal, it’s less likely they’ll be able to steal from you and you’ll make it take longer for the player closest to winning to win.

In order to do this I created 4 “strategies”, 2 for picking: BasicPickStrategy and PickColorsLeastLikelyToBeStolenStrategy and then 2 for stealing: BasicStealStrategy and StealColorsLeastLikelyToBeStolenStrategy. Each player needs one of each strategy, which conveniently results in 4 players to cover all possible combinations:

  • Player 0: BasicPickStrategy, BasicStealStrategy
  • Player 1: BasicPickStrategy, StealColorsLeastLikelyToBeStolenStrategy
  • Player 2: PickColorsLeastLikelyToBeStolenStrategy, BasicStealStrategy
  • Player 3: PickColorsLeastLikelyToBeStolenStrategy, StealColorsLeastLikelyToBeStolenStrategy

I then had the program play 1,000,000 games between the 4 players, randomizing the order of the players:
$ ./sneaky_snacky_squirrel.py 1000000

The output was:
mean,std,wins
22.121029,8.412456,Counter({3: 296961, 1: 289553, 0: 207117, 2: 206369})

That means on average it took 22.1 turns with a standard deviation of 8.4 to complete a game and Player 3 won 29.7% of the time, Player 1 won 29.0% of the time, Player 0 won 20.7% of the time and Player 2 won 20.6% of the time. So it seems that a non-naive stealing strategy makes the most difference and a non-naive picking strategy also has a small positive effect. There are certainly other strategies, if you’d like to try them just add a new strategy to the code below. I’ve also posted unit tests so you can be more confident things are augmented properly.

#!/usr/bin/python

import itertools
import random
import sys
from collections import Counter
from collections import defaultdict
from math import sqrt


class Color(object):
  """Enumeration of all possible colors"""
  COLORS = set(range(0, 5))
  BLUE, GREEN, PURPLE, RED, YELLOW = COLORS


class SpinResult(object):
  """Enumeration of all possible spin results."""
  RESULTS = set(range(0, 10))
  BLUE, GREEN, PURPLE, RED, YELLOW, WIND, PICK_ONE, PICK_TWO, STEAL, SLEEP = (
      RESULTS)


def GetColorCounts(player, all_players):
  """Count all the colors all the other players have.

  Args:
    player: The player to exclude from the counts.
    all_players: All the players in the game.

  Returns:
    A dict of color->count.
  """
  color_counts = defaultdict(int)
  for other_player in all_players:
    if player == other_player:
      continue  # Don't count player's colors.
    for color in other_player.log:
      color_counts[color] += 1
  return color_counts


class BasicPickStrategy(object):
  """A naive acorn picking strategy."""

  def __init__(self, all_players):
    """Constructor.
  
    Args:
      all_players: A list of all Player objects playing the game.
    """
    self.all_players = all_players

  def pick(self, player, num_acorns=1):
    """Pick the first acorn color needed.
    
    Args:
      num_acorns: The number of acorns to pick.
    """
    if player.filled_log():
      return
    available_colors = Color.COLORS.difference(player.log)
    while num_acorns > 0 and available_colors:
      player.log.add(available_colors.pop())
      num_acorns -= 1


class BasicStealStrategy(object):
  """A naive acorn stealing strategy."""

  def __init__(self, all_players):
    """Constructor.
  
    Args:
      all_players: A list of all Player objects playing the game.
    """
    self.all_players = all_players

  def steal(self, player):
    """Steal the first acorn color needed from the first viable player..
    
    Args:
      player: The player performing the steal.
    """
    if player.filled_log():
      return
    for other_player in self.all_players:
      if player == other_player:
        continue
      available_colors = other_player.log.difference(player.log)
      if available_colors:
        picked_color = available_colors.pop()
        player.log.add(picked_color)
        other_player.log.remove(picked_color)
        break


class PickColorsLeastLikelyToBeStolenStrategy(BasicPickStrategy):
  """A less naive acorn picking strategy."""

  def __init__(self, all_players):
    """Constructor.
  
    Args:
      all_players: A list of all Player objects playing the game.
    """
    BasicPickStrategy.__init__(self, all_players)

  def pick(self, player, num_acorns=1):
    """Pick the most prevalent color acorns; they're least likely to be stolen.
    
    Args:
      num_acorns: The number of acorns to pick.
    """
    if player.filled_log():
      return
    available_colors = Color.COLORS.difference(player.log)
    color_counts = GetColorCounts(player, self.all_players)
    # Pick the most prevalent color(s).
    for color in sorted(color_counts, key=color_counts.get, reverse=True):
      if num_acorns > 0 and color in available_colors:
        player.log.add(color)
        available_colors.remove(color)
        num_acorns -= 1
    # If colors owned by other players are exhasted, pick any needed colors.
    while num_acorns > 0 and available_colors:
      player.log.add(available_colors.pop())
      num_acorns -= 1


class StealColorsLeastLikelyToBeStolenStrategy(BasicStealStrategy):
  """A less naive acorn stealing strategy."""

  def __init__(self, all_players):
    """Constructor.
  
    Args:
      all_players: A list of all Player objects playing the game.
    """
    BasicStealStrategy.__init__(self, all_players)

  def steal(self, player):
    """Steal most prevalent color acorn from the player with the most acorns.
    
    Args:
      player: The player performing the steal.
    """
    if player.filled_log():
      return
    available_colors = Color.COLORS.difference(player.log)
    color_counts = GetColorCounts(player, self.all_players)
    players = sorted(self.all_players, key=lambda p: len(p.log),
        reverse=True)
    for color in sorted(color_counts, key=color_counts.get, reverse=True):
      if color in available_colors:
        player.log.add(color)
        for p in players:
          if color in p.log:
            p.log.remove(color)
            break


class Player(object):
  """Represents a player."""

  def __init__(self, pick_strategy_cls, steal_strategy_cls, number):
    """Constructor.
  
    Args:
      pick_strategy_cls: A class to use for picking acorns.
      steal_strategy_cls: A class to use for stealing acorns.
      number: A unique identifier.
    """
    self.log = set()
    self.pick_strategy_cls = pick_strategy_cls
    self.steal_strategy_cls = steal_strategy_cls
    self.pick_strategy = None
    self.steal_strategy = None
    self.number = number

  def reset(self, all_players):
    """Reset the state of the player.

    Args:
      all_players: A list of Player objects playing the game.
    """
    self.log = set()
    self.pick_strategy = self.pick_strategy_cls(all_players)
    self.steal_strategy = self.steal_strategy_cls(all_players)

  def filled_log(self):
    """Returns True if the player has filled their log, otherwise False."""
    return len(self.log) == len(Color.COLORS)

  def spin(self, value):
    """Process a spin of the wheel.

    Args:
      value: The value of the spin.

    Returns:
      The spun value.
    """
    if value < len(Color.COLORS):
      # Add the selected colored acorn to player's log.
      self.log.add(value)
    elif value == SpinResult.WIND:  # Lose all acorns.
      self.log = set()
    elif value == SpinResult.PICK_ONE:  # Pick any colored acorn.
      self.pick_strategy.pick(self)
    elif value == SpinResult.PICK_TWO:  # Pick any 2 colored acorn.
      self.pick_strategy.pick(self, 2)
    elif value == SpinResult.STEAL:  # Steal an acorn from any other player.
      self.steal_strategy.steal(self)
    elif value == SpinResult.SLEEP: # Skip player's turn.
      pass
    return value

  def __str__(self):
    return  '[%d, %s, %s]' % (self.number, self.pick_strategy_cls.__name__,
        self.steal_strategy_cls.__name__)


def play(players):
  """Plays a single game.

  Args:
    players: List of players playing the game.

  Returns:
    The number of spins performed to complete the game and the winning player.
  """
  num_spins = 0
  for player in players:
    player.reset(players)
  game_over = False
  winner = None
  while not game_over:
    for player in players:
      value = player.spin(random.randint(0, 9))
      num_spins += 1
      game_over = player.filled_log()
      if game_over:
        winner = player
        break

  return num_spins, winner


def mean(data):
  """Compute the mean of data."""
  n = len(data)
  return sum(data) / float(n)


def stddev(data):
  """Compute the standard deviation of data."""
  c = mean(data)
  ssd = sum((x - c) ** 2 for x in data)
  v = ssd / (len(data) - 1)
  return sqrt(v)


def trials(players, num_games):
  """Play games and compute basic statistics.
  
  Args:
    players: A list of Player objects playing the game.
    num_games: The number of games to play when computing statistics.

  Returns:
    Mean number and standard deviation of spins needed to finish a game.
  """
  game_result = []  # (number of_spins, winning player #)
  while num_games > 0:
    random.shuffle(players)
    game_result.append(play(players))
    num_games -= 1
  spin_results = [x[0] for x in game_result]
  winners = [x[1].number for x in game_result]
  return (mean(spin_results), stddev(spin_results), Counter(winners))


if __name__ == '__main__':
  """Play a bunch of games and output the results."""
  num_games = int(sys.argv[1])
  pick_strategies = [BasicPickStrategy, PickColorsLeastLikelyToBeStolenStrategy]
  steal_strategies = [BasicStealStrategy,
      StealColorsLeastLikelyToBeStolenStrategy]
  strategy_combinations = list(
      itertools.product(pick_strategies, steal_strategies))
  players = []
  for i, strategies in enumerate(strategy_combinations):
    players.append(Player(strategies[0], strategies[1], i))
  mean_spins, stddev_spins, win_counts = trials(players, num_games)
  print 'mean,std,wins'
  print '%f,%f,%s' % (mean_spins, stddev_spins, win_counts)

Here are some unit tests:

#!/usr/bin/python

import random
import unittest

from sneaky_snacky_squirrel import BasicPickStrategy
from sneaky_snacky_squirrel import BasicStealStrategy
from sneaky_snacky_squirrel import Color
from sneaky_snacky_squirrel import PickColorsLeastLikelyToBeStolenStrategy
from sneaky_snacky_squirrel import Player
from sneaky_snacky_squirrel import SpinResult


class FakePickStrategy(object):

  def __init__(self, all_players):
    self.pick_called_with = (None, None)

  def pick(self, player, num_acorns=1):
    self.pick_called_with = (player, num_acorns)


class FakeStealStrategy(object):

  def __init__(self, all_players):
    self.steal_called_with = None

  def steal(self, player):
    self.steal_called_with = player


class TestPickColorsLeastLikelyToBeStolenStrategy(unittest.TestCase):

  def setUp(self):
    self.players = set()
    self.player_1 = Player(PickColorsLeastLikelyToBeStolenStrategy,
      FakeStealStrategy, 1)
    self.player_2 = Player(PickColorsLeastLikelyToBeStolenStrategy,
      FakePickStrategy, 2)
    self.player_3 = Player(PickColorsLeastLikelyToBeStolenStrategy,
      FakePickStrategy, 3)
    self.players.add(self.player_1)
    self.players.add(self.player_2)
    self.players.add(self.player_3)
    self.player_1.reset(self.players)
    self.player_2.reset(self.players)
    self.player_3.reset(self.players)

  def testPick(self):
    random_color = random.randint(0, len(Color.COLORS) - 1)
    self.player_2.log.add(random_color)
    self.player_3.log.add(random_color)
    random_color_1 = None
    while True:
      random_color_1 = random.randint(0, len(Color.COLORS) - 1)
      if random_color_1 != random_color:
        self.player_2.log.add(random_color_1)
        break
    self.player_1.pick_strategy.pick(self.player_1)
    self.assertEqual(1, len(self.player_1.log))
    self.assertTrue(random_color in self.player_1.log)
    self.player_1.spin(SpinResult.WIND)
    self.assertEqual(0, len(self.player_1.log))
    self.player_1.pick_strategy.pick(self.player_1, num_acorns=2)
    self.assertEqual(2, len(self.player_1.log))
    self.assertTrue(random_color in self.player_1.log)
    self.assertTrue(random_color_1 in self.player_1.log)


class TestBasicPickStrategy(unittest.TestCase):

  def testPick(self):
    strategy = BasicPickStrategy(None)
    player = Player(None, None, None)
    self.assertEqual(0, len(player.log))
    strategy.pick(player)
    self.assertEqual(1, len(player.log))
    strategy.pick(player, num_acorns=4)
    self.assertEqual(5, len(player.log))
    strategy.pick(player)
    self.assertEqual(5, len(player.log))


class TestBasicStealStrategy(unittest.TestCase):

  def testSteal(self):
    self.players = set()
    self.player_1 = Player(FakePickStrategy, BasicStealStrategy, 1)
    self.player_2 = Player(FakePickStrategy, BasicStealStrategy, 2)
    self.players.add(self.player_1)
    self.players.add(self.player_2)
    self.player_1.reset(self.players)
    self.player_2.reset(self.players)
    self.player_1.steal_strategy.steal(self.player_1)
    self.assertEqual(0, len(self.player_1.log))
    self.assertEqual(0, len(self.player_2.log))
    random_color = random.randint(0, len(Color.COLORS) - 1)
    self.player_2.log.add(random_color)
    self.player_1.steal_strategy.steal(self.player_1)
    self.assertEqual(1, len(self.player_1.log))
    self.assertEqual(0, len(self.player_2.log))
    self.assertTrue(random_color in self.player_1.log)
    self.player_2.log.add(random_color)
    self.player_1.steal_strategy.steal(self.player_1)
    self.assertEqual(1, len(self.player_1.log))
    self.assertEqual(1, len(self.player_2.log))
    self.assertTrue(random_color in self.player_1.log)
    self.assertTrue(random_color in self.player_2.log)


class TestPlayer(unittest.TestCase):

  def setUp(self):
    self.player = Player(FakePickStrategy, FakeStealStrategy, 1)
    self.player.reset(None)

  def testFilledLog(self):
    self.player.log.add(Color.BLUE)
    self.assertFalse(self.player.filled_log())
    self.player.log.update(Color.COLORS)
    self.assertTrue(self.player.filled_log())
    self.assertEqual((None, None), self.player.pick_strategy.pick_called_with)
    self.assertIsNone(self.player.steal_strategy.steal_called_with)

  def testSpinSpecificColor(self):
    value = random.randint(0, len(Color.COLORS) - 1)
    self.player.spin(value)
    self.assertEqual(1, len(self.player.log))
    self.assertTrue(value in self.player.log)
    self.assertEqual((None, None), self.player.pick_strategy.pick_called_with)
    self.assertIsNone(self.player.steal_strategy.steal_called_with)

  def testSpinWind(self):
    self.player.log.add(random.randint(0, len(Color.COLORS) - 1))
    self.assertEqual(1, len(self.player.log))
    self.player.spin(SpinResult.WIND)
    self.assertEqual(0, len(self.player.log))
    self.player.log.update(Color.COLORS)
    self.assertEqual(len(Color.COLORS), len(self.player.log))
    self.player.spin(SpinResult.WIND)
    self.assertEqual(0, len(self.player.log))
    self.assertEqual((None, None), self.player.pick_strategy.pick_called_with)
    self.assertIsNone(self.player.steal_strategy.steal_called_with)

  def testSpinPickOneColor(self):
    self.player.spin(SpinResult.PICK_ONE)
    self.assertEqual(
        (self.player, 1), self.player.pick_strategy.pick_called_with)
    self.assertEqual(0, len(self.player.log))
    self.assertIsNone(self.player.steal_strategy.steal_called_with)

  def testSpinPickTwoColors(self):
    self.player.spin(SpinResult.PICK_TWO)
    self.assertEqual(
        (self.player, 2), self.player.pick_strategy.pick_called_with)
    self.assertEqual(0, len(self.player.log))
    self.assertIsNone(self.player.steal_strategy.steal_called_with)

  def testSpinSteal(self):
    self.player.spin(SpinResult.STEAL)
    self.assertEqual(self.player, self.player.steal_strategy.steal_called_with)
    self.assertEqual(0, len(self.player.log))
    self.assertEqual((None, None), self.player.pick_strategy.pick_called_with)

  def testSpinSleep(self):
    self.player.spin(SpinResult.SLEEP)
    self.assertEqual(0, len(self.player.log))
    self.assertEqual((None, None), self.player.pick_strategy.pick_called_with)
    self.assertIsNone(self.player.steal_strategy.steal_called_with)


if __name__ == '__main__':
  unittest.main()

June 16th, 2017

Fearless Jeter Bunny Salami

25hrs and 43min, the time period between 6:46a Sunday May 14, 2017 (when my flight touched down) to 8:30a Monday May 15, 2017 (when my return flight took off). Witnessing the ceremony to retire Derek Jeter’s #2 occupied a portion of time around 6:35p +/- travel time to and from Yankee Stadium, but what about the rest?

Bright And Early Landing

Traveling alone with a small bag afforded a great deal of flexibility but many places did not open early on Sunday, limiting my pre-10a options. Arriving in JFK and relying on the subway meant traveling through Brooklyn to get to Manhattan and then the Bronx. My chosen path took me past the Barclay’s Center…hosting a Falun Dafa get together along with protesters.

Protest Outside Falun Dafa Conference

Justice At The Crossroads greeted me at the start of my 2mi walk along 5th Ave through the Park Slope neighborhood.

Justice At The Crossroads

Not many establishments open before 9a on a Sunday and I wanted something I could eat for breakfast while walking, which lead me to Bagel World. The bagel itself did not impress, though the bacon and chive cream cheese made it worthwhile.

Bagel World Cream Cheese Selection

At the start of my walk, the pizza places I passed by touted brick ovens or wood fires, but as I left Park Slope, the advertisements changed.

Pizza And Spanish Food

Eventually my destination came into view. Green-wood Cemetery contains the graves of Henry Chadwick the “Father of Baseball” and Charles Ebbets who orchestrated the construction of Ebbets Field, home of the former Yankees’ rival Brooklyn Dodgers.

Green-wood Cemetery Entrance

Father Of Baseball

Not only did that align with the baseball theme of my trip, but my dad mentioned a noteworthy sculpture, Triumph of Civic Virtue i.e. Naked Man Trampling Mermaids, also called the cemetery home and fortunately it stood near the other grave markers. All three landmarks cluster along a ~1mi path from the main entrance to 20th St and Prospect Park West (as a bonus Leonard Bernstein’s grave also abuts the path).

Naked Man Trampling Mermaids

After the cemetery, I made my way to Grand Army Plaza along the west side of Prospect Park, walking briskly to arrive by 10:30a while finishing my bagel. I wanted to make a video call to my daughter to show her one of the landmarks from Knuffle Bunny Too in a feeble attempt to demonstrate the fun of traveling. The original plan for this trip was for the whole family to go to the game, visit my grandmother and perhaps other friends and family, but she does not like to travel and be away from home which makes the week leading up to the trip, the trip itself and the following week mostly miserable.

Grand Army Plaza

I took the 3 train to Wall St. Upon exiting the subway I noticed crowds of tourists taking photos of a building. To my surprise, our current president owns it, 40 Wall St aka The Trump Building.

Trump Building

The Fearless Girl statue caused quite a stir after its installation almost 2 months earlier. I wanted to take a photo of my daughter with the statue, but being alone I instead captured a few random tourists and after waiting a long time gave up on there ever being a break in the crowd.

Strike A Pose With Fearless Girl

As sometimes happens, the Metropolitan Transit Authority’s service advisories fooled me into thinking certain trains were available at certain stations. Alas, the first three stations I attempted to enter to catch a 4/5/6 train uptown were closed. However the search caused me to pass by the statue a second time and while someone troubleshot a camera malfunction, I managed to sneak in.

Fearless Girl

I ended up on the R and eventually met my dad at the Metropolitan Museum of art. We played Ingress for a bit, each making progress towards the MAGNUS Builder Medal and then decided to head towards Yankee Stadium.

At 2:38a the previous day, the Yankees sent an e-mail to ticket holders of Sunday’s game. They decided to cancel Saturday’s game due to predicted inclement weather and made Sunday a single admission doubleheader. The first game started at 2:05p, the ceremony followed (though no earlier than 6:35p) and game two began shortly after that. We decided against spending 8+ hours of the trip at the stadium, but still needed to eat some lunch. A quick search for “pizza near Yankee Stadium” turned up “167th St Pizza”.

167th St Pizza

The best? It certainly tasted different, “sweet” (as in not sour, salty, bitter etc. as opposed to cool, awesome, rad).

Best Pizza In The Bronx?

On the way back to the stadium, we passed by another pizza place, Giovanni’s and tried one of their slices. It tasted more normal, though no one needs to drop everything and go there.

Giovanni's Pizza

However, the next time I’m in the area I’ll check this place out.

The Other KFC

We arrived at the end of the 6th inning of game 1 with the Yankees ahead by a run. They immediately lost the lead in the next inning, giving up 3 runs after a pitching change. A cranky fan in front of us provided some colorful entertainment, continuing the rants about the pitching change even after the Yankees scored 6 in the bottom half to take the lead for good. Nice of the team to welcome Derek Jeter and company with a win.

Numerous highlights from Jeter’s career played on the video screen. Part of me wished they also had a mini roast of lowlights, but perhaps that’s inappropriate? I can remember a number of games I attended where he struck out (or worse hit into a double play [higher than the MLB average, especially from 2007 onward]) with the bases loaded.

That said, he’s also responsible for 2 of the most memorable plays I’ll probably ever witness in person. The Flip and the first MLB home run hit in November. I certainly didn’t dislike Jeter, but I think I probably rooted more for each of the other 3 members of The Core Four. However, I missed each of their number retirement ceremonies as my short trips back to the East Coast diminished greatly with the birth of my daughter. There are certainly more important things in life than professional sports, especially if you’re not involved in them other than as a fan but I made the decision last year that barring any serious emergency, I would not miss his.

Derek Jeter's Number Retirement Ceremony

Derek Jeter Chant

If you’ve made it this far, surely you have one question on your mind, how much money did I put on my MetroCard for a day in NYC? There’s a $1 fee to get a new MetroCard (sadly all my previous ones had expired). Each subway ride costs $2.75. The AirTrain ride from JFK costs $5. One receives a 5% bonus each time one adds at least $5.50 to the card (no bonus on the $1 fee). http://enterprise.mtanyct.info/MetroCardCalculator/ shows how many rides one can get for a given amount of money, as well as various dollar amounts that result in some number of rides leaving the card with a $0.00 balance. It does not compute the minimum amount of money to put on a card if one knows exactly how many rides will be taken.

For my trip I estimated 4 rides + 1 AirTrain + 1 new Metrocard so x = 4, y = 1 and I needed to solve for z: (x * 2.75 + 5 * y) / 1.05 + 1 = z. z = 16.2381 However, one can only purchase in increments of $0.05 so solving for z’ = z – (z % 0.05) + 0.05, z’ = $16.25 which would leave me with $0.0125 on the MetroCard. I actually made two purchases (since I did not start thinking about this immediately) the first for $8.50 which yielded a MetroCard worth $7.88 ($8.50 – $1 fee = $7.50, then getting the 5% bonus yielded $7.875 and MTA rounded that up to $7.88. Then I added $7.75, the 5% bonus yields $8.1375 and the MTA rounded up again, giving me $8.14. This left me with $0.02 on my card at the end of the trip…if I had made a single $16.25 transaction, only $0.01 would remain. Using the calculator it appears the MTA rounds to 1 cent when the remainder is >= 0.5, regardless of whether the previous number is even or odd (enter $6.50, $6.65 and $6.70 to see). With a maximum value of $80 for a new pay-per-ride Metrocard, one could make 14 purchases for $5.70 each ($79.80 total) and end up with $83.86 on the card (5.087719298% bonus) vs a single $79.80 purchase resulting in $83.79. If someone thinks of a way to automate and re-distribute cards, they could engage in some https://en.wikipedia.org/wiki/Salami_slicing.

February 5th, 2017

Burger King Is Out Of Burgers

Just over 20 years ago, I arrived in Berkeley California for college. 1996 was an election year, though not remotely as contentious at the presidential level as the most recent one. At the time, talk of red state/blue state would have brought to mind
Sandra Boynton not Clinton Dole. However at the state level, proposition 209 (repealing affirmative action) sparked a bitter fight and it’s passage (55% – 45%) spawned mass demonstrations at and around UC Berkeley.

All the recent chatter about the 1960s Berkeley Free Speech Movement living on or dying last week during the MILO riots reminded me that while not an overtly political person, I attended a rally denouncing prop 209 mainly out of curiosity…perhaps a rite of passage as a new Bezerklean. IIRC at the time I believed in the importance of diversity (and still do) but not strongly enough to make a sign or paint my face. Here are a few photos:

Prop 209 Protest Sproul Plaza UC Berkeley 1996-11-06

Prop 209 Protest March Down Bancroft Way Berkeley 1996-11-06

Circumstances dictated the laughable quality of the images (maybe someday I’ll look for the prints and re-scan):

  1. The camera used was a cheap Vivitar point-and-shoot.
  2. There were only a couple of flat bed scanners on campus available to the general student population and they required signing up in advance for 1 of only a few 15 minute time-slots per week.
  3. I did not get into on-campus housing i.e. no ethernet, and the images were originally served over the web from my personal computer via a 14.4kbps dial-up connection.

It’s not quite, “back in my day I used punch cards” but the university provided two forms of internet connectivity to off-campus students; the Warhol pool of 32 19.2kbps lines that automatically disconnected you after 15 minutes, and the main home Internet Protocol (HIP) pool of 256 14.4kbps lines that would allow one to stay connected indefinitely as long as it though you were “active” (setting the Eudora e-mail program to check for new mail every 15 minutes seemed to do the trick). They even had a convenient subdomain registration so anytime my web server was running it could be accessed at http://figment.hip.berkeley.edu (someday I’ll mirror the full contents on madpickles.org).

Unlike the potential repeal of the America Cares Act i.e. Obamacare, I don’t remember discussions regarding any replacement for affirmative action. After its passage the makeup of the student body predictably, drastically changed. I didn’t know it at the time, but looking back at a geographic map of yes/no votes, counties the San Francisco Bay Area and Los Angeles voted no and all others voted yes. Carter and Mondale (Democrats) also both carried San Francisco and Alameda counties in the 1980 and 1984 elections against former governor Reagan (Republican). Perhaps more than just the 3rd wave of punk rock drew me to Berkeley, I suppose most of my beliefs at that time (and still now) lean towards what would be deemed modern US liberalism with touches of libertarianism thrown in.

I don’t remember my level of agitation after the the 2000 election; if I had to guess it would have been mild considering I was a new grad at a dot-com company in the midst of the bubble bursting and didn’t have much of a clue (nor a care) as to how bad the new administration might be. A few years later in 2003 I went out to document a protest (war in Iraq), this time making a short video with some tongue in cheek accompaniment. Again, this happened mainly out of curiosity as while certainly not pro-war I don’t remember being furious about the war. In 2008 I remember attending a meetup of people looking to help with the Obama campaign, but the focus seemed to be on affecting Nevada which was more effort than I was willing to put in. The last political related thing I remember doing was writing an analysis of prop 8 data (legalize gay marriage) after the 2008 election.

After much agonizing about how big of a disaster it might be to try and take our toddler to the Women’s March Oakland on January 21st 2017, my wife and I got on our bikes with our daughter and headed to the North Berkeley BART station around 9:30a, figuring we’d be more likely get on a train at a stop further from Oakland. As we got closer, people walked past us heading away from the station. Once the station came into view we saw a line of people around the block waiting to get in. Time being of the essence, I didn’t even stop to take a photo, just turned around and headed back home as we knew there was an AC Transit 88 bus scheduled to arrive by our house soon that would get us within half a mile of the march’s meeting point. We got on a bus around 10:10a and it slowly filled up:

Women's March Oakland 2017-01-21

We made it to the mass of people, stood around with them for a while and ran into a couple of people we knew.

Women's March Oakland 2017-01-21

Women's March Oakland 2017-01-21

After about 30 minutes our daughter couldn’t take the situation anymore, there was no marching, and one of her ears was hurting (not from noise, but from an oncoming illness). So shortly after 11:30a we made our way out of the crowd and headed towards the 12th st Oakland BART station. We stopped at a Walgreens to get some children’s Tylenol and then to a Burger King as the idea of a hamburger appealed to her (and I had brought coupons, of course). There were a few people in front of us and as we got closer to ordering one of the workers announced they were out of burgers, but still had fish and chicken. Burger King…out of…burgers. Welcome to the new administration.