public
Description: Computer implementation of Robert Brandom's "game of giving and asking for reasons," from Making It Explicit, chapter 3.
Homepage:
Clone URL: git://github.com/jgm/gogar.git
gogar / gogar.rb
100755 636 lines (566 sloc) 18.261 kb
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
#!/usr/bin/env ruby
# Gogar.rb - gogar in ruby
#
# GOGAR - the game of giving and asking for reasons.
# A partial simulation of the scorekeeping dynamics from Chapter 3
# of Robert Brandom's book Making It Explicit (Harvard, 1994).
# (c) 2006 John MacFarlane. This software carries no warranties
# of any kind.
 
require 'set'
require 'webrick'
require 'cgi'
include WEBrick
 
class Set
  def to_s
    "{" + self.to_a.join(", ") + "}"
  end
end
 
class EqSet < Set
  def add item
    if not any? {|x| x == item}
      super.add item
    end
  end
 
  def delete item
    super.delete_if {|x| x == item}
  end
end
 
class Inference
  def initialize(premises, conclusion) # premises a list
    @premises = EqSet.new premises
    @conclusion = conclusion
  end
  attr_reader :premises, :conclusion
  
  def == inf
    (@premises == inf.premises) && (@conclusion == inf.conclusion)
  end
  
  def to_s
    @premises.to_s + " |- " + @conclusion.to_s
  end
end
 
class InferenceSet < EqSet
  def to_s
    self.to_a.join("; ")
  end
end
 
class IncompatibilitySet < EqSet
  def to_s
    self.to_a.join("; ")
  end
end
 
class Challenge
  def initialize(target, sentence)
    @target = target
    @sentence = sentence
  end
  
  attr_reader :target, :sentence
 
  def == chal
    @target == chal.target && @sentence == chal.sentence
  end
  
  def to_s
    "challenged #{target.name}'s entitlement to \"#{sentence}\""
  end
end
 
class ChallengeSet < EqSet
  def to_s
    self.to_a.join("; ")
  end
end
 
def wrap(str, indent=" ")
  indent = " "
  endline = "\n"
  width = 78
  pos = 0
  outstring = ""
  sep = ", "
  str.split(/; |, /).each do |s|
    if pos != 0 and (pos + s.length + sep.length) > width
      outstring += endline
      outstring += indent
      pos = indent.length
    end
    outstring += s
    outstring += sep
    pos += (s.length + sep.length)
  end
  sep_pos = -1 * sep.length
  if outstring.length >= sep.length # strip off trailing sep if there is one
    outstring[0..(sep_pos-1)]
  else
    outstring
  end
end
 
class Agent
  def initialize(name,
                 committive = [[["A is red"],"A is colored"],
                                [["A is blue"],"A is colored"],
                                [["A is green"],"A is colored"]],
                 permissive = [[["A is red", "A is fragrant"], "A is edible"],
                               [["A is blue", "A is small"], "A is poisonous"]],
                 incompatibles = [["A is red", "A is blue"],
                               ["A is red", "A is green"],
                               ["A is blue", "A is green"],
                               ["A is edible", "A is poisonous"]])
    @name = name
    @intelligence = 100
    @commitments_avowed = EqSet.new []
    @incompatibilities = IncompatibilitySet.new(incompatibles.map {|i| EqSet.new i})
    @committive_inferences = InferenceSet.new(committive.map {|p,c| Inference.new p, c})
    @permissive_inferences = InferenceSet.new(permissive.map {|p,c| Inference.new p, c})
    @challenges_issued = ChallengeSet.new []
  end
 
  attr_accessor :name, :intelligence, :commitments_avowed,
  :incompatibilities, :committive_inferences, :permissive_inferences,
  :challenges_issued
 
  def asserts str
    @commitments_avowed.add str
  end
 
  def disavows str
    @commitments_avowed.delete str
  end
 
  def challenges(agent, str)
    if agent.commitments_avowed.member? str
      @challenges_issued.add Challenge.new(agent, str)
    else
      # wasn't asserted
    end
  end
  
  def withdraws_challenge(agent, str)
    @challenges_issued.delete Challenge.new(agent,str)
  end
  
  def to_s
    name +
      "\nIntelligence = #{intelligence}" +
      wrap("\nCommitments avowed: #{commitments_avowed.to_s}") +
      wrap("\nSets taken to be incompatible: #{incompatibilities.to_s}") +
      wrap("\nCommittive inferences accepted: #{committive_inferences.to_s}") +
      wrap("\nPermissive inferences accepted: #{permissive_inferences.to_s}") +
      wrap("\nChallenges issued: #{challenges_issued.to_s}") +
      "\n"
  end
end
 
class Game
  def initialize
    @agents = EqSet.new []
    @transcript = []
  end
 
  attr_accessor :agents, :transcript
 
  def reset
    agents.each { |ag| agents.delete ag }
    @transcript = []
  end
 
  def agent_named(name)
    agents.select { |ag| ag.name.downcase == name.downcase }[0]
  end
 
  def assertions_challenged(agent)
    challenged_sentences = EqSet.new []
    agents.each { |a| challenged_sentences.merge a.challenges_issued.select {|c| c.target == agent}.map { |c| c.sentence }}
    challenged_sentences
  end
  
  def assertions_unchallenged(agent)
    agent.commitments_avowed - assertions_challenged(agent)
  end
 
  def all_unchallenged_assertions
    all = EqSet.new []
    self.agents.each{|a| all = all | assertions_unchallenged(a)}
    all
  end
 
  def consequences_once(infs, basis)
    # returns set of consequences of applying inference to sentences in basis
    conclusions = EqSet.new
    infs.each do |inf|
      if inf.premises.subset? basis
        conclusions.add inf.conclusion
      end
    end
    basis | conclusions
  end
  
  def compatible_with?(incompatibilities, commitments, sentence)
    # returns true iff sentence is compatible with commitments according to incompatibilities
    all = commitments | EqSet.new([sentence])
    not incompatibilities.any? {|inc| inc.subset?(all) && (not inc.subset?(all - sentence)) } # sentence is ruled out only if it MAKES incompatibility where there wasn't any before
  end
  
  def remove_incompatibles(set, commitments, incompatibilities)
    # returns result of removing sentences from set that are incompatible
    # with sentences in commitments
    set.find_all {|s| compatible_with?(incompatibilities, commitments, s) }.to_set
  end
    
  def consequences_once_and_prune(infs, base, coms, incs)
    remove_incompatibles(consequences_once(infs, base), coms, incs)
  end
  
  def fixed_point(set, times, &block)
    if times == 0
      set
    else
      nxt = block.call set
      if set == nxt
        set
      else
        fixed_point(nxt, (times - 1), &block)
      end
    end
  end
  
  def commitments(scorekeeper, other)
    # returns set of commitments scorekeeper attributes to other
    fixed_point(other.commitments_avowed, scorekeeper.intelligence) {|set| consequences_once(scorekeeper.committive_inferences, set)}
  end
  
  def incompatibles(scorekeeper, other)
    # returns set of sets of incompatible sentences that other is committed to,
    # according to scorekeeper
    IncompatibilitySet.new scorekeeper.incompatibilities.select {|inc| inc.subset? commitments(scorekeeper,other)}
  end
 
  def entitlements(scorekeeper, other)
    # returns the entitlements scorekeeper attributes to other
    coms = commitments(scorekeeper, other)
    incs = scorekeeper.incompatibilities
    times = other.intelligence
    # start with default entitlement to anything anyone has asserted, unless
    # it has been challenged or is incompatible with other's commitments
    base = remove_incompatibles(self.all_unchallenged_assertions, coms, incs)
    fixed_point(base, times) {|s| expand_entitlements(coms, s, incs, scorekeeper.committive_inferences, scorekeeper.permissive_inferences, times)}
  end
 
  def expand_entitlements(coms, ents, incs, cominfs, perminfs, times)
    # apply committive inferences, which are all permissive too,
    # removing incompatibles afterward
    ents2 = consequences_once_and_prune(cominfs, ents, coms, incs)
    # close under permissive inferences from entitled commitments only
    ents3 = consequences_once_and_prune(perminfs, (ents & coms), coms, incs)
    ents2 | ents3
  end
 
  def score(scorekeeper, other)
    # returns string with score of scorekeeper on other in game
    coms = commitments(scorekeeper, other)
    ents = self.entitlements(scorekeeper, other)
    incs = incompatibles(scorekeeper, other)
    wrap("\nCommitments: #{coms.to_s}") +
      wrap("\nEntitlements: #{ents.to_s}") +
      wrap("\nIncompatibles: #{incs.to_s}") +
      "\n\n"
  end
 
  def score_all
    # returns score of all on all
    agents = self.agents.to_a.sort_by {|a| a.name}
    agents.map {|sk| agents.map {|ot| "#{sk.name}'s score on #{ot.name}" + self.score(sk, ot)}}.join("")
  end
 
  def self.startup
    "
Welcome to the game of giving and asking for reasons,
a simulation of the linguistic scorekeeping dynamics
described in chapter 3 of Robert Brandom's book
Making It Explicit (Harvard University Press, 1994).
 
(c) 2006 John MacFarlane
 
For a list of sample commands, type help
 
"
  end
 
  def command(inp)
    # returns string result of processing inp in game
    result = case
    when inp =~ /^\s*(quit|exit)\s*$/
      "Goodbye.\n"
    when inp =~ /^\s*help\s*$/
      help_message
    when inp =~ /^\s*(list)?\s*agents\s*$/
      self.agents.map {|ag| ag.to_s}.join("\n")
    when inp =~ /^\s*add\s*agent\s+(\w+)\s*$/
      name = remove_quotes($1)
      if self.agent_named(name)
        "An agent named #{$1} already exists.\n"
      else
        self.agents.add Agent.new(name)
        "Agent #{$1} added.\n"
      end
    when inp =~ /^\s*remove\s*agent\s+(\w+)\s*$/
      name = remove_quotes($1)
      ag = self.agent_named(name)
      if ag
        self.agents.delete ag
        "Agent #{$1} removed.\n"
      else
        "There is no agent named #{$1}.\n"
      end
    when inp =~ /^\s*new\s*game\s*$/
      self.reset
      self.agents.add Agent.new("Ann")
      self.agents.add Agent.new("Bob")
      Game.startup
    when (inp =~ /^\s*score\s*of\s+(\w+)on\s+(\w+)\s*$/ ||
          inp =~ /^\s*(\w+)'s\s+score\s+on\s+(\w+)\s*$/)
      unless scorekeeper = self.agent_named($1)
        "Agent #{$1} not found. Try: list agents\n"
      else
        unless other = self.agent_named($2)
          "Agent #{$2} not found. Try: list agents\n"
        else
          self.score(scorekeeper, other)
        end
      end
    when inp =~ /^\s*score\s*$/
        self.score_all
    when inp =~ /^\s*(\w+)\s+asserts:?\s*([^\.]+)\.?\s*$/
      sentence = remove_quotes($2)
      ag = self.agent_named($1)
      unless ag
        "Agent #{$1} not found. Try: list agents\n"
      else
        ag.asserts sentence
        self.score_all
      end
    when inp =~ /^\s*(\w+)\s+disavows:?\s*([^\.]+)\.?\s*$/
      sentence = remove_quotes($2)
      ag = self.agent_named($1)
      unless ag
        "Agent #{$1} not found. Try: list agents\n"
      else
        unless ag.commitments_avowed.member? sentence
          "Agent #{$1} has not asserted \"#{sentence}\"\n"
        else
          ag.disavows sentence
          self.score_all
        end
      end
    when inp =~ /^\s*(\w+)\s+challenges\s+(\w+)('s\s+entitlement\s+to)?\s+([^\.]+)\.?\s*$/
      sentence = remove_quotes($4)
      ag = self.agent_named(remove_quotes($1))
      target = self.agent_named(remove_quotes($2))
      unless ag
        return "Agent #{$1} not found. Try: list agents\n"
      else
        unless target
          "Agent #{$2} not found. Try: list agents\n"
        else
          unless target.commitments_avowed.member? sentence
            "#{target.name} never asserted \"#{sentence}\"\n"
          else
            ag.challenges_issued.add(Challenge.new(target, sentence))
            self.score_all
          end
        end
      end
    when inp =~ /^\s*(\w+)\s+(abandons|withdraws)\s+(his\s+|her\s+|its\s+)?challenge\s+(to\s+)?(\w+)('s entitlement to)?\s+([^\.]+)\.?\s*$/
      sentence = remove_quotes($7)
      ag = self.agent_named(remove_quotes($1))
      target = self.agent_named(remove_quotes($5))
      unless ag
        "Agent #{$1} not found. Try: list agents\n"
      else
        unless target
          "Agent #{$5} not found. Try: list agents\n"
        else
          ag.withdraws_challenge(target, sentence)
          self.score_all
        end
      end
    when inp =~ /^\s*(\w+)\s+(adds|removes)\s+(committive|permissive)\s+inference:?\s+(\[|\{)?\s*([^\]\}]+)\s*(\]|\})?\s*\|-\s*([^\.]+)\.?\s*$/
      ag = self.agent_named($1)
      unless ag
        "Agent #{$1} not found. Try: list agents\n"
      else
        prems = $5.strip
        conc = $7.strip
        type = $3
        which = $2
        premises = prems.split(/\s*,\s*|\s*;\s*/)
        inf = Inference.new(premises, conc)
        if type == "permissive"
          if which == "adds"
            ag.permissive_inferences.add inf
          else
            ag.permissive_inferences.delete inf
          end
        else # committive
          if which == "adds"
            ag.committive_inferences.add inf
          else
            ag.committive_inferences.delete inf
          end
        end
        self.score_all
      end
    when inp =~ /^\s*(\w+)\s+(adds|removes)\s+incompatibility:?\s+(\[|\{)?\s*([^\]\}]+)\s*(\]|\})?\s*\.?\s*$/
      ag = self.agent_named($1)
      unless ag
        "Agent #{$1} not found. Try: list agents\n"
      else
        which = $2
        sents = $4.split(/\s*,\s*|\s*;\s*/)
        incomps = EqSet.new(sents)
        if which == "adds"
          ag.incompatibilities.add incomps
        else # removes
          ag.incompatibilities.delete incomps
        end
        self.score_all
      end
    when inp =~ /^\s*(\w+)\s*$/
      ag = self.agent_named($1)
      if ag
        ag.to_s + "\n" + self.score(ag, ag)
      else
        "Command not recognized. Try: help\n"
      end
    when true
      "Comand not recognized. Try: help\n"
    end
    self.transcript << [inp, result]
    result
  end
    
end
 
def testgame
  # returns test game
  game = Game.new
  game.agents.add Agent.new("Ann")
  game.agents.add Agent.new("Bob")
  game
end
 
def remove_quotes string
  string.gsub('"', '')
end
 
def help_message
  "\nlist agents\nadd agent Sal\nremove agent Bob\nnew game\nscore\nBob's score on Ann\nBob asserts A is red\nBob disavows A is red\nAnn challenges Bob's entitlement to A is red\nAnn abandons his challenge to Bob's entitlement to A is red\nBob adds committive inference: A is red; A is small |- A is dangerous\nBob adds incompatibility: {A is red; A is yellow}\nAnn removes incompatibility: {A is blue; A is red}\nAnn removes permissive inference: A is small; A is blue |- A is edible\nAnn\nhelp\nquit\n\n"
end
 
class GogarServlet < HTTPServlet::AbstractServlet
 
  @@nextsession, @@games =
    begin
      Marshal.load(CGI.unescape(File.read("gogar.data")))
    rescue
      [0, {}]
    end
  
  def self.newsession
    self.savesessions
    @@nextsession += 1
    return @@nextsession
  end
 
  def self.savesessions
    File.new("gogar.data", 'w').print(CGI.escape(Marshal.dump([@@nextsession, @@games])))
  end
  
  def template
    ERB.new %q{
<html>
<head>
<title>GOGAR</title>
<link href="gogar.css" media="all" rel="Stylesheet" type="text/css" />
</head>
<body>
<form action="/" method="post">
<p><label for="command">Command</label><br/>
<input type="text" name="command" size="40"/></p>
</form>
<pre><%= answer %>
</pre>
<a href="/?transcript">Full transcript of session</a>
</body>
</html>}, 0, "%<>"
  end
 
  def transcript_template
    ERB.new %q{
<html>
<head>
<title>GOGAR Transcript</title>
</head>
<body>
<h2>Transcript</h2>
<a href="/">Back to GOGAR</a>
<pre><% transcript.each do |item| %>
<% command = item[0] %>
<% answer = item[1] %>
<b><%= command %></b>
 
<%= answer %>
<% end %>
</pre>
</body>
</html> }, 0, "%<>"
  end
  
  def do_GET(req, res)
    cookie = req.cookies.find {|c| c.name == 'gogar'}
    if cookie && @@games[cookie.value.to_i]
      session = cookie.value.to_i
      answer = "Welcome back! You can start where you left off,
or start a new game by typing 'new game'\n"
    else
      session = GogarServlet.newsession
      @@games[session] = testgame
      res.cookies.push([Cookie.new('gogar', session.to_s)])
      answer = Game.startup
    end
    game = @@games[session]
    res['Content-Type'] = "text/html"
    res.status = 200
    if req.query['transcript']
      page = transcript_template
      transcript = game.transcript
    else
      page = template
    end
    res.body = page.result(binding)
  end
 
  def do_POST(req, res)
    cookie = req.cookies.find {|c| c.name == 'gogar'}
    if cookie && @@games[cookie.value.to_i]
      session = cookie.value.to_i
    else
      session = GogarServlet.newsession
      @@games[session] = testgame
      res.cookies.push([Cookie.new('gogar', session.to_s)])
    end
    game = @@games[session]
    command = req.query['command'].gsub(/</,"&lt;").gsub(/>/,"&gt;") || ""
    res['Content-Type'] = "text/html"
    res.status = 200
    answer = game.command(command)
    res.body = template.result(binding)
  end
end
  
# main loop
 
if __FILE__ == $0
 
  require 'optparse'
 
  options = {}
  opts = OptionParser.new do |opts|
    opts.program_name = 'GOGAR (c) 2006 John MacFarlane'
    opts.version = 'version 1.0'
    opts.banner = "GOGAR -- the game of giving and asking for reasons
Usage: ruby gogar.rb [options]"
    opts.on("-w", "--web [PORT]", Integer,
            "Run web version of GOGAR [on port PORT]") do |port|
      options[:port] = port || 9094
    end
    opts.on("-v", "--version", "Show version") do
      puts opts.ver
      exit 0
    end
  end
 
  begin
    opts.parse!
  rescue OptionParser::ParseError => e
    puts e.message
    puts opts.help
    exit 1
  end
 
  if options[:port] # web version
    puts "Starting web version of GOGAR on http://localhost:#{options[:port]}"
    puts "Type Ctrl-C to stop."
 
    s = HTTPServer.new(:Port => options[:port])
 
    s.mount("", GogarServlet)
    trap("INT") { GogarServlet.savesessions; s.shutdown }
    s.start
  
  else # console version
    begin
      require 'readline'
      with_readline = true
    rescue LoadError
      with_readline = false
    end
 
    game = Game.new
    output = nil
    print game.command("new game")
    while output != "Goodbye.\n"
      if with_readline
        com = Readline.readline("GOGAR> ", true).chomp
      else
        print "GOGAR> "
        com = readline.chomp
      end
      output = game.command(com)
      print output
    end
  end
end