'From Squeak3.8 of ''5 May 2005'' [latest update: #6665] on 27 October 2005 at 9:41:14 am'! Object subclass: #ShipCaptainCrew instanceVariableNames: 'throwsLeft dice rollThisDie ship captain crew' classVariableNames: '' poolDictionaries: '' category: 'msh-games'! !ShipCaptainCrew commentStamp: 'msh 10/26/2005 20:57' prior: 0! Introduction This class is a representation of the classic bar game: Ship, Captain, Crew (or SCC for short.) It's traditionally played with five 6 sided dice. Now the last time I saw a die was when I was in Vegas for DefCon last year, so I thought it would be fun to code up a Smalltalk version. The rules of the game are simple: 0. Each player has three throws. The goal is to roll a 6, 5 and 4 in these three rolls. The 6 represents the "ship." The 5 represents the "captain." And the 4 represents the "crew." The remaining dice are totalled to yeild the player's score. If a player goes through three throws and doesn't get a 6, 5 and a 4, their score is 0. 1. On each throw, the player may choose to roll any of his five dice, he or she can even decide to roll none of their dice. (more on why you would want to do this later) 2. When the player throws a 6, this die is termed "the ship." Note that it's possible on the first throw to throw all sixes. This is where the rules lawers get in a snit. Some people say you MUST re-roll all but one of the dice; others say if you feel lucky, you can just reroll two. Personally, I don't take sides on this issue; I say heck... reroll all the dice if you want to, but it's probably a bad strategy to give up a perfectly good ship. 3. When the player throws a 5, this die is termed "the captain." Note that you have to have rolled your ship on or before the current roll. So even if you get all fives on the first roll, it does you no good. You MUST roll your ship before you roll your captain. 4. In the same way that the 6 represents the ship and the 5 represents the captain, the 4 represents "the crew" (or if you're from Louisiana, "the crewe.") In the same way that you have to roll the ship before (or at the same time) you roll your captain, you have to roll your captain before (or at the same time) you roll your crew. 5. The remaining dice are totalled to yeild your "cargo" score. At the end of each round of play, the player with the highest score wins. Players with the same score usually use fisticuffs or pistols at twenty paces to resolve the tie. Note that captains can be rolled on the same throw as ships and crews on the same throw as captains. If you roll a 6, 5 and 4 on your first throw, bully for you!! You just got your ship, captain and crew on a single roll. Likewise... if you roll a 6 and a 5 on your first roll, you got your ship and captain on the first roll. Set these dice aside, you don't want to re-roll them in the next throw. The thing you can't do is roll a 5 and a 4 but not a 6 on your first roll and then claim them for your captain and crew. You've got to roll the 6 on the same or a previous roll to rolling the 5. Ditto for rolling the 5 and the 4. A Few Words on Strategy If you're a game theory geek like myself, you're probably busily drawing decision trees and calculating win probabilities based on well defined strategies. Good for you. If you're playing this game in the bar with some friends from work, it's entirely possible you might not have brought your laptop with you to calculate the odds. So here are a few rules of thumb. 1. Don't be a fool. Don't re-roll the dice that represent your ship, captain, or crew. 2. If you get your ship early, and your non-ship dice are high valued, consider NOT re-rolling the high valued dice and hoping that you get your captain and crew with the dice you do roll. A classic example of this occurs when you roll all sixes in your first roll. Sure it's unlikely (1 in 7776) but it is possible. If you're playing a round and you're the last player and someone in the previous round scored an 11, you may want to re-roll two dice and pray you get a 4 and a 5. My back of the envelope calculation tells me you have about a 5% chance of winning with this strategy versus about a 2% chance of rolling a 5, 4 and two 6's by re-rolling all four dice twice. But these type of calculations routinely confuse people and I had a few beers before sitting down to write this documentation, so you really should work these strategies out for yourself. Using This Class This class simulates three throws of five dice. To create a new instance of this class, simply invoke the #new method as so: ShipCaptainCrew new To simulate throwing the dice, you call the #throw method. If you throw the dice more than three times, nothing changes until you call the #reinitialize method. So to simulate two rounds, you could do something like this: ( ShipCaptainCrew new ) throw; throw; throw; reinitialize; throw; throw; throw. After each throw, this implementation checks to see if you rolled a ship, captain or crew. If you did, it does not re-roll that die in subsequent throws. To get your score, invoke the #score method like so: ( ShipCaptainCrew new ) throw; throw; throw; score After each throw, the #evaluate method is called. This is the method that looks at the previous roll and decides which dice to re-roll. Currently we mark the ship, captain and crew so as not to re-roll them. If you have a better idea for a strategy, feel free to subclass this class and implement the #evaluate method. Implementation Notes So the implementation is pretty straight-forward. We have a bevy of instance variables: throwsLeft - This is the number of throws you have left. It's decremented each time you invoke #throw. The #throw method is smart enough to check to see if throwsLeft is non-positive. dice - This is an array holding five integers representing the dice. rollThisDie - This is an array of booleans. Each boolean is associated with the integer in the dice array and represents whether we'll roll that die during the next throw. ship, captain and crew - These variables are integers representing the index into the dice array for the ship, captain and crew. If you haven't rolled a ship, captain or crew yet, these values will be zero. Extending This Class This class is intended to be extended. I was hoping someone would write a ShipCaptainCrewMorph using this guy as the model. I tried to put all the methods of interest to user interface developers in the accessing and user interface categories. Also... I use the default system PRNG. If you want trusted randomness, you'll probably want to override the classes in the randomosity category. My Hidden Shame Okay... it's not terribly hidden. The default user interface is to print stuff out to the Transcript. I am ashamed.! ShipCaptainCrew subclass: #SimpleStrategySCC instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'msh-games'! !SimpleStrategySCC commentStamp: '' prior: 0! This is a simple subclass of ShipCaptainCrew that implements a strategy. Instead of blithely re-rolling all dice that aren't a ship, captain or crew, this class keeps rolls that are greater than 3 (assuming you already have your ship, captain and crew.)! !ShipCaptainCrew methodsFor: 'accessing' stamp: 'msh 10/26/2005 20:57'! captain ^ captain .! ! !ShipCaptainCrew methodsFor: 'accessing' stamp: 'msh 10/26/2005 19:08'! captain: anInteger captain := anInteger. self dontRollDie: anInteger.! ! !ShipCaptainCrew methodsFor: 'accessing' stamp: 'msh 10/26/2005 20:57'! crew ^ crew .! ! !ShipCaptainCrew methodsFor: 'accessing' stamp: 'msh 10/26/2005 19:08'! crew: anInteger crew := anInteger. self dontRollDie: anInteger.! ! !ShipCaptainCrew methodsFor: 'accessing' stamp: 'msh 10/26/2005 20:58'! dontRollDie: anInteger rollThisDie at: anInteger put: false.! ! !ShipCaptainCrew methodsFor: 'accessing' stamp: 'msh 10/26/2005 20:58'! rollDie: anInteger rollThisDie at: anInteger put: true.! ! !ShipCaptainCrew methodsFor: 'accessing' stamp: 'msh 10/26/2005 20:58'! ship ^ ship .! ! !ShipCaptainCrew methodsFor: 'accessing' stamp: 'msh 10/26/2005 19:08'! ship: anInteger ship := anInteger. self dontRollDie: anInteger.! ! !ShipCaptainCrew methodsFor: 'accessing' stamp: 'msh 10/26/2005 17:52'! throwsLeft: anInteger "We put this one in so that when someone uses this class as the model for a MVC or" "a Morph, they can overload it and add a self changed: #throwsLeft: message." throwsLeft := anInteger.! ! !ShipCaptainCrew methodsFor: 'user interface' stamp: 'msh 10/26/2005 13:01'! noThrowsLeft "Okay... this guy should be overridden. Currently all we do is print the score to the" "Transcript. Probably not the best decision in the world." Transcript nextPutAll: '%SCC-S-DONE; No throws left. Your score is (' , ( self score ) asString , ')'; cr; flush.! ! !ShipCaptainCrew methodsFor: 'user interface' stamp: 'msh 10/27/2005 09:35'! postThrow "In case you have to do something after each throw." Transcript nextPutAll: '%SCC-I-RESULTS; '. 1 to: (rollThisDie size) do: [ :current | ( rollThisDie at: current ) ifFalse: [ Transcript nextPut: $(. ]. Transcript nextPutAll: ( ( dice at: current ) asString ) . ( rollThisDie at: current ) ifFalse: [ Transcript nextPut: $). ]. ( current = (rollThisDie size) ) ifFalse: [ Transcript nextPutAll: ', '. ]. ]. Transcript nextPutAll: '.'; cr; flush. ! ! !ShipCaptainCrew methodsFor: 'user interface' stamp: 'msh 10/26/2005 18:30'! preFirstThrow "In case you have to do something before the first throw." Transcript nextPutAll: '%SCC-I-BEGIN; Starting a round of Ship, Captain and Crew.'; cr; flush.! ! !ShipCaptainCrew methodsFor: 'user interface' stamp: 'msh 10/26/2005 19:33'! preThrow "In case you have to do something before each throw (even called before the first throw," "immediately after #preFirstThrow.)" "Transcript nextPutAll: '%SCC-I-THROW; Throws left: ' , ( throwsLeft asString ) , '.'; cr; flush."! ! !ShipCaptainCrew methodsFor: 'game' stamp: 'msh 10/26/2005 20:59'! evaluate "Override this method if you want to develop a new strategy for automatic play." ( ship = 0 ) ifTrue: [ self find: 6 withGetter: #ship withSetter: #ship: notZero: #( ) ]. ( captain = 0 ) ifTrue: [ self find: 5 withGetter: #captain withSetter: #captain: notZero: #( #ship ) ]. ( crew = 0 ) ifTrue: [ self find: 4 withGetter: #crew withSetter: #crew: notZero: #( #ship #captain ) ].! ! !ShipCaptainCrew methodsFor: 'game' stamp: 'msh 10/27/2005 09:36'! find: anInteger | location | location := 0. 1 to: (dice size) do: [ :current | ( ( dice at: current ) = anInteger ) ifTrue: [ location := current . ] . ] . ^ location .! ! !ShipCaptainCrew methodsFor: 'game' stamp: 'msh 10/26/2005 18:46'! find: anInteger withGetter: getter withSetter: setter notZero: anArray | location doIt | doIt _ true. anArray do: [ :current | ( ( self perform: current ) = 0 ) ifTrue: [ doIt _ false . ] ]. doIt ifTrue: [ ( ( self perform: getter ) = 0 ) ifTrue: [ location := self find: anInteger. ( location = 0 ) ifFalse: [ self perform: setter with: location. ]. ]. ]. ! ! !ShipCaptainCrew methodsFor: 'game' stamp: 'msh 10/27/2005 09:36'! score | return | return := 0. ( self stillNeedShipCaptainOrCrew ) ifFalse: [ 1 to: (dice size) do: [ :current | ( (ship = current) or: (captain = current) or: (crew = current) ) ifFalse: [ return := return + ( dice at: current). ]. ]. ]. ^ return .! ! !ShipCaptainCrew methodsFor: 'game' stamp: 'msh 10/26/2005 19:25'! stillNeedShipCaptainOrCrew ^ ( ( ship * captain * crew ) = 0 ) .! ! !ShipCaptainCrew methodsFor: 'game' stamp: 'msh 10/27/2005 09:37'! throw ( throwsLeft > 0 ) ifTrue: [ self preThrow. 1 to: (rollThisDie size) do: [ :current | ( rollThisDie at: current ) ifTrue: [ self throwDie: current ]. ]. self throwsLeft: ( throwsLeft - 1 ). self evaluate. self postThrow. ( throwsLeft <= 0 ) ifTrue: [ self noThrowsLeft. ]. ] ifFalse: [ self noThrowsLeft. ].! ! !ShipCaptainCrew methodsFor: 'game' stamp: 'msh 10/26/2005 20:59'! throwDie: anInteger dice at: anInteger put: ( self atRandom: 6 ).! ! !ShipCaptainCrew methodsFor: 'initialization' stamp: 'msh 10/27/2005 09:34'! initialize "Default initialization. Create a container with 5 dice. Calling ShipCaptainCrew>>new: will" "give you an instance with something other than 5 dice." self initialize: 5.! ! !ShipCaptainCrew methodsFor: 'initialization' stamp: 'msh 10/27/2005 09:33'! initialize: anInteger "Initialize this instance with anInteger number of dice." dice := Array new: anInteger. rollThisDie := Array new: anInteger. self reinitialize .! ! !ShipCaptainCrew methodsFor: 'initialization' stamp: 'msh 10/27/2005 09:34'! reinitialize "This method reinitializes a SCC context." throwsLeft := 3. ship := 0. captain := 0. crew := 0. 1 to: (rollThisDie size) do: [ :current | rollThisDie at: current put: true . ]. self preFirstThrow.! ! !ShipCaptainCrew methodsFor: 'randomosity' stamp: 'msh 10/26/2005 12:42'! atRandom: anInteger "This method returns an integer between 1 and anInteger (inclusive). The default" "implementation simply uses the Integer>>atRandom: method. If the subclass wants to do" "something more random, it should overload this method." ^ anInteger atRandom .! ! !ShipCaptainCrew methodsFor: 'randomosity' stamp: 'msh 10/26/2005 12:39'! initializeRandomness "Subclasses that need to do something special for initializing a random number generator" "should overload this method."! ! !ShipCaptainCrew class methodsFor: 'instance creation' stamp: 'msh 10/27/2005 09:32'! new: anInteger "Create a new instance with anInteger number of dice (instead of the default which is 5)." ^ ( self new ) initialize: anInteger .! ! !SimpleStrategySCC methodsFor: 'game' stamp: 'msh 10/27/2005 09:40'! evaluate "Here's where the magic happens. First we call evaluate in our parent to ensure we don't" "re-roll our ship, captain or crew. If we have all of them, we check to see if the cargo" "dice are a 4, 5 or 6. If so, we keep them." super evaluate. ( self stillNeedShipCaptainOrCrew ) ifFalse: [ 1 to: (rollThisDie size) do: [ :current | ( rollThisDie at: current ) ifTrue: [ ( ( dice at: current ) >= 4 ) ifTrue: [ self dontRollDie: current. ]. ]. ]. ]. ! ! !ShipCaptainCrew class reorganize! ('instance creation' new:) !