Agora Nomic's wiki. Learn more at https://www.agoranomic.org/.
Note that due to elements of code in this essay it is reproduced in PRE format.
A Completely Formal Nomic
Submitted in partial fulfilment of the
requirements for the degree of Bachelor
of Nomic. This Thesis is associated with
the recent Proposal "A B.N. for favor".
Player Favor, Agora Nomic
Introduction
Historically, many of the scams and CFJs in Agora Nomic
have hinged on matters of word-meaning. Everyday language
is notoriously ambiguous and dependant on assumptions
about speaker and listener intent that are not logically
well-founded; they require the application of what is
often called "common sense" (a loose and imperfectly-shared
collecion of default assumptions, word-usage rules, and so on).
The Rules and Customs of Agora Nomic have historically
had an ambivalent attitude towards common sense. Judges
have sometimes been allowed or required to exercise it
when the Rules are not clear. Some Players have cited the
imperfect consensus on matters of common sense in borderline
cases, and tried to reduce or eliminate the Rules' dependence
on it.
CFJs and scams based on word usage are welcomed and relished
by some Players, and despised by others. Some have accused
the community of making scams impossible by encouraging
Judges to block them by quibbles on word meaning; others
have claimed that without such quibbles, most scams would
be impossible in the first place.
It would be illuminating to consider what a Nomic game
might be like that was wholely lacking in disputes and
disagreements about word usage and meaning, or about
common sense or other broad subjective notions. Such
a game would be interesting by virtue of the contrasting
light it would throw on Agora and similar Nomics, as well
as potentially leading to interesting games in its own right.
So
The idea is to design a completely formal Game, one in which
there are no rules that have subjective components, or are
otherwise open to disputes about common sense or the
meanings of words. The game should also be as Nomic-like
as possible, given that restriction, in that the rules of
the game should be represented within, and changeable by
moves of, the game.
This will *not* be "real" Nomic unless we are wildly more
successful than I think we will be. Because we have to
choose a comparatively simple formal framework (in order
to keep it comprehensible), it's unlikely that we'll
be able to get as "meta" as non-formal Nomic can get.
It will also, of course, not have the fun of Judgement
and dispute about word meanings and intent and sense
and reference and all that that most Nomics offer. This
is intended to be a *different* game, albeit somewhat
inspired by the usual Nomic. But there's no claim that it's
better than any other Nomic, or truer to any particular ideal,
or anything like that.
There will, of course, have to be an interface between
the formal system in which the rules sit and the players,
who are out in the physical world. This interface will be
referred to as the Mechanics of the game, and these will
be subject to all sorts of disputes and things. But they
are *not* part of the game itself; any issues that come up
about the Mechanics ("I didn't make that move; someone must
have forged the e-mail!") will be extra-game issues, and
will not be expressed or resolved in the gamestate.
Similarly, the language in which the game is expressed will
be subject to change (and in particular to expansion) as
the game goes on, but this again is an extra-game process.
As new verbs or conditions or whatever are added to the
language, we will just have discovered new parts of the
formal space that the game is exploring. The status of
the language of the game is not contained in the game
itself. This is another limitation that nonformal Nomic
does not, at least in theory, suffer from. I suspect,
however, that a strong case could be made for the claim
that any nonformal Nomic played by humans will in fact
be subject to the same sort of limitation, in that humans,
being finite entities instantiated in a limited universe,
cannot ultimately escape all bounds of language themselves.
Relationship to Agora Nomic
Formal Nomic can serve Agora Nomic as a thought-pump, if
nothing else. If some actual or contemplated feature of
Agora Nomic would be especially hard or easy to add to
a hypothetical game of Formal Nomic, or would require
diddling with the Mechanics, that says something about
the nature of the feature. If two apparently-similar
ways of embodying within Agora the same basic idea turn
out to have very different implications for a hypothetical
game of Formal Nomic, that says something about the
differences between them.
Formal Nomic might also be a useful, or instructive,
or amusing subgame (probably a Contest) within Agora
Nomic. One Player would implement (either automatically,
or by hand) the Mechanics, and choose an Initial Set;
the other Players in the jurisdiction of the subgame
would submit input to the Mechanics, changing the state
of the subgame. Winning the subgame could mean being
awarded the subgame's Treasury, or whatever.
Details / Examples
The rest of this paper consists of a worked-out example
of one completely formal Nomic, including a description
of the mechanics, and an Inital Set (that is, an initial
object-pool). It is of course only one example; the
basic objective could be accomplished in many other
ways. For instance, if all the Players were willing
to be computer programmers, the ruleset and gamestate
could be expressed directly as computer program texts
and variable values, at a considerable increase in power,
and a considerable cost in difficulty of play.
For this example, the incoming interface is relatively
simple: when a piece of mail comes in for the "server",
it's parsed into one or more objects in the system.
Each object has a Type of "move", a Sender and Timestamp
filled in from the envelope, a randomly-generated BatchNumber
that's the same for all the objects created by the same piece
of mail, and other fields filled in from the body of the
message. For instance, a request to register might look like:
From: Matt Zimola <[email protected]>
Date: 14 Aug 1995, 12:36:13 UTC
Subject: to the Mech server
REGISTER MattZ
and the corresponding object that gets created might be:
objectId: 127
batchId: 114
moveTimeStamp: 1995/08/14 12:36:13
moveSender: [email protected]
type: move
subtype: register
nickname: MattZ
"REGISTER MattZ" is syntactic sugar, to make a common operation
easier. In its full generality, the incoming Mechanics would
allow the sender of the mail to specify any attributes and values
e wants to (except type, objectId, batchId, and so on).
On the other end, there will be a small set of verbs that
cause extra-game side-effects; primarily mail-sending to let the
physical-world players know what's going on. But these side-effects
are not defined in or modifiable by the game rules; they are
the Mechanics, not the Rules. From the point of view of the
game-state itself, the sendFoo() verbs will be no-ops.
A rule is just another object, with Type "rule", an "if"
attribute which is some boolean function of the gamestate,
and a "then" attribute which is some sequence of verbs.
It also has a ruleOrder, to determine which rules get to
look at the game-state first, and probably various other
housekeeping numbers. (More generally, we can have a
special engineSettings object which would tell the engine
which type of objects are really rules; that would normally
be set to "rule", but could be diddled with for various
special effects.)
The sequence of events in the game is as follows. Note that
this is again a thing that isn't part of, or changable by,
the Rules, and this is another un-Nomic-like limitation.
It may be possible to make some or all of this stuff adjustable
within the game also, with some effort. (Note that that effort
would have to be done outside the game, in the Mechanics, to
enable laters efforts within the game.)
- Something happens: either a new object arrives from the
physical world via the incoming interface, or a rule
suddenly fires due to a schedule pop.
- For each object of Type "rule" in the gamestate, in order
of ruleOrder, the value of the "if" attribute is
evaluated. If the result if "false", we proceed to the
next "rule" object. If the result is "true", the verbs in the
"then" attribute fire, creating or deleting or modifying or
scheduling objects; then we restart this "for" loop, back at
the rule with the highest (lowest-numbered) PrecendenceNumber.
(If a rule fires, but the "then" clause does *not* change
the game-state, we don't restart.)
- Once the highest-ruleOrder rule has had its "if" part
evaluate to "false", we go to sleep again until the next
thing happens.
There are all sorts of "programming" issues involved in making these
things actually work. When a rule fires and changes the game-state,
for instance, it will almost always have to make sure that the change
it makes is such that it won't fire again for the same reasons, since
its change will cause a restart, and if no rule with a lower ruleOrder
fires, it will see the game-state again just the way it left it. So
for instance a rule that gives a player a point because of some
condition should make sure to also make the condition false, otherwise
it may give the player an infinite number of points, one at a time!
(Another aspect of the Mechanics will presumably be an extra-Game
meta-rule like "if the game-state goes into a verifiable loop,
the last player to submit a move wins".)
One way to manage this is to have a rule simply delete the object
that caused it to fire; this is sensible in some cases, although it
will often be better to simply mark it in some way (set the
processedByRule47 attribute to T, for instance), rather than deleting
it, so that other rules with higher ruleOrders will get a chance
to act on it as well.
Below is a sample Initial State. It includes registration (but not
deregistration or On Hold), proposals (which may contain multiple
Rule Changes), voting (but not multiple votes, or changing votes),
quorum, simple majority (AIs and MIs are quite doable, but are not
in this Initial State), points for voting and having your proposals
pass, and a simple Win on points (which ends the game). Kudos,
degrees, Win Tokens, Extra Votes, the Potato, and some other familiar
Agora concepts would fit into this set in reasonably simple ways.
Some other concepts are much harder; for instance, the idea
of sub-rulesets that only apply to some Players, as in Group
Ordinances and Contest Regulations. Platonic sub-rules are
especially hard to formalize. For instance, if a Group
Ordinance says that in some circumstances a Group Member
shall transfer fifteen Marks to a non-Member, that's
acceptable. If it says the opposite, that some non-Member
shall transfer fifteen Marks to some Member, that isn't.
In both cases, both a Member and non-Member appear in the
subrule; formalizing the difference between the two is a
subject for further study.
I haven't actually *implemented* any of this as a computer program,
and I'm not sure that I have any interest in doing so. But I hope
it will spur some interesting discussion nonetheless...
A note on scams: an amusing proof that scams are entirely
possible in this formal Nomic. In the first draft of this Initial
State, Rule 16 didn't set the "cleanup" attribute of incoming votes
to F. So if you submitted a vote, and set "cleanup" to T in the
move object, you'd get your point for voting, but the vote would
be destroyed by the Sweeper, so you could do it again. And again.
And again. Getting a point each time, and winning at will. Easy
to fix, but illustrative! (In the very first version of Rule 16,
it didn't even have the check to ensure that the proposal ID was
valid, making it easy to win by voting for a hundred non-existent
proposals.)
Anyway:
Basic attributes of objects:
objectId - a number, used as a handle for talking
about an object, assigned sequentially by the system
when the object is created. Match clauses always
find the matching object with the lowest objectId.
type - tells what it is; the incoming interface produces only
objects of type "move". The rule engine gets its settings
from the first objects of type "engineControls", and
runs only objects whose type is the same as the runType
attribute of engineControls. All other types have meaning only to
rules. A basic type in the initial set is "player".
Move attributes set by the incoming interface (these attributes
are always assigned; any and all other attributes besides Type
can be set by the incoming mail explicitly):
moveSender - [email protected] that sent the mail
moveTimeStamp - yyyymmddhhmmss (a time-format string), from
the date: line of the mail
moveBatch - a contentless id assigned to all the "move"
objects that come in in the same piece of mail (for
batching up multiple Rule Changes into a single proposal,
etc)
Except for these three, and type (which is always "move"), the
sender of the mail can control all the attributes of the move object.
Syntactic sugar of various kinds will presumably be provided for
common cases.
Rule attributes used by the engine:
ruleOrder - rules are run in order of ruleOrder, lowest to highest.
if - condition tested when a rule is run.
then - verbs executed when an if is true; if this causes any
change to the existing objects (creation, deletion,
amendment), the ruleset is restarted.
Some verbs:
delete(match) - deletes the matching object (with the lowest
objectId, as usual)
create(match) - creates a matching object
set(match1)(match2) - Changes the object matching match1 so
that it matches match2.
send([email protected][....])(expression) - queues up the value
of the expression for sending to [email protected] (or list thereof).
sendObject([email protected][...])(match)(string) - queues up a
print form of the matching object, with the string for a
header, for sending.
sendNow() - actually sends all queued stuff off.
Some functions for use in expressions:
exists(match)
count(match)
timeGE(timevalue) - Tests if the current time is >= the given
time. Used for timers. A possible engine optimization:
duing each pass, keep track of the lowest-value timeGE
that fails; set an actual timer to re-check all rules
at that time, if nothing else has happened in the meantime.
Notes:
- Some sort of "for" clause in rules may be needed for performance
and clarity reasons. Otherwise, the only way to do something
like sending the entire ruleset to a player in response to a
query move would be to do passes through the
first part of the ruleset, using some kludged-up bookkeeping
attribute or object to keep track of which rules we haven't
sent yet. Maybe that's OK?
- Perhaps allow a rule to say that the engine *shouldn't* restart
the ruleset after processing it? That, instead, it should just
test the rule itself again, and then proceed onwards?
- A more radical change to this model would be to have all
object-changes take place in a second, "shadow", copy of
the object-pool, and switch over to that object-pool only
when no rule in the current pool wants to make any changes.
Could be an engineControls switch?
- Probably be good to have a setall(match1)(match2), that would
efficiently set all objects matching match1 to match match2.
An Intial State
---------------
Loose conventions about ruleOrder:
We will use ruleOrder 10000-19999 for rules that don't delete
the object that sets them off (although they may set some
attributes so that it doesn't set them off again).
We will use ruleOrder 30000-39999 for rules that do delete, or
otherwise make very significant changes (like changes of type)
to the object that sets them off.
We will use ruleOrder 99000-99999 for rules that should basically
only run for cleanup-type reasons at the very end of an event.
Initial engine settings and (empty) player-list:
objectId: 1
type: engineSettings
runType: rule
objectId: 2
type: playerList
listContents: ""
Rules about registration:
objectId: 3
ruleOrder: 30010
type: rule
if: exists(type=="move" & subtype=="register" & moveSender==%s &
nickname!="" & nickname==%n & moveTimeStamp==%tm & objectId==%mo) &
!exists(type=="player" & nickname==%n) &
exists(type=="playerList" & listContent==%pl)
then: create(type=="player" & defaultEmail==%s & nickname==%n &
registeredAt==%tm & score==0 & objectId==%po) &
set(objectId==%mo)(processed==T & cleanup==T) &
create(type=="newPlayer" & reference==%po & cleanup==T) &
send(%pl,"New player" %n "registered at" %tm) &
send(%s,"Welcome to Engine City," %n"!")
comment: "if someone's registering, and e gave a nickname that
isn't already in use, create a proxy object for em with
their information, note that the move was processed for
later cleanup, and create a newPlayer object for other
rules to process. Also tell everyone."
objectId: 4
ruleOrder: 30020
type: rule
if: exists(type=="move" & subtype=="register" & processed!=T &
moveSender==%s & nickname!="" & nickname==%n & objectId==%mo) &
exists(type=="player" & nickname==%n)
then: sendObject(%s)(objectId==%mo)("A player" %n "is already registered") &
delete(objectId==%mo)
comment: "handle error case where someone tries to register with a
name that's already in use."
objectId: 5
ruleOrder: 30030
type: rule
if: exists(type=="move" & subtype=="register" & moveSender==%s &
nickname=="" & objectId==%mo) &
then: sendObject(%s)(objectId==%mo)("You have to give a nickname when you
register") & delete(objectId==%mo)
comment: "handle error case where someone tries to register without
a name."
objectId: 6
ruleOrder: 30040
type: rule
if: exists(type=="move" & from=="" & moveSender==%s & objectId==%o) &
exists(type=="player" & defaultEmail==%s & nickname==%n)
then: set(objectId==%o)(from==%n)
comment: "if no from is given on a move, guess from the email address"
objectId: 7
ruleOrder: 30050
type: rule
if: exists(type=="move" & from==%f & moveSender==%s & objectId==%mo) &
!exists(type=="player" & nickname==%f)
then: sendObject(%s)(objectId==%mo)("You are no known player.") &
delete(objectId==%mo)
objectId: 8
ruleOrder: 30060
type: rule
if: exists(type=="newPlayer" & listed!=T & defaultEmail==%e &
objectId==%po) &
exists(type=="playerList" & listContent==%l & objectId==%lo)
then: set(objectId==%po)(listed==T) &
set(objectId==%lo)(listContent==%l %e)
comment: "Maintain a list of all player email addresses, for
mass sendings"
A simple Win rule:
objectId: 9
ruleOrder: 10010
type: rule
if: exists(type=="player" & score>100 & nickname==%n & objectId==%o) &
!exists(type=="win" & who==%o) &
exists(type=="playerList" & listContent==%pl)
then: create(type=="win" & who==%o) &
send(%pl,%n "wins the game!")
comment: "if a player has over 100 points, e wins"
objectId: 10
ruleOrder: 10020
type: rule
if: exists(type=="win")
then: halt()
comment: "the game ends when one or more players win"
Rules about Proposing:
objectId: 11
ruleOrder: 30120
type: rule
if: exists(type=="move" & subtype=="ruleChange" &
ruleChangeType=="repeal" & target==%t &
batchid==%b & objectId==%mo) &
exists(objectId==%to & type=="rule")
then: set(objectId==%mo)(type="changeBeingVotedOn")
comment: "snarf in an incoming repeal rule change; we'll
let ones that attempt to repeal a non-existing rule just
fall through to the general error handler at first."
objectId: 12
ruleOrder: 30130
type: rule
if: exists(type=="move" & subtype=="ruleChange" &
ruleChangeType=="amend" & target==%t &
batchid==%b & objectId==%mo) &
exists(objectId==%to & type=="rule")
then: set(objectId==%mo)(type="changeBeingVotedOn")
comment: "snarf in an incoming amendment rule change"
objectId: 13
ruleOrder: 30140
type: rule
if: exists(type=="move" & subtype=="ruleChange" &
ruleChangeType=="create" & target==%t &
batchid==%b & objectId==%mo)
then: set(objectId==%mo)(type="changeBeingVotedOn")
comment: "snarf in an incoming create rule change"
objectId: 14
ruleOrder: 30150
type: rule
if: exists(type=="changeBeingVotedOn" & sent!=T & batchId==%b &
objectId==%o) &
exists(type=="playerList" & listContent==%pl)
then: sendObject(%pl,%o,"Proposed rule-change (proposal" %b"):") &
set(objectId==%o)(sent==T)
comment: "Tell all players about new proposals"
objectId: 15
ruleOrder: 30160
type: rule
if: exists(type="changeBeingVotedOn" & batchid==%b) &
!exists(type=="voteInProgress" & propId==%b)
then: create(type=="voteInProgress" & propId==%b) &
expiryTime==timeNow()+10*24*60*60)
comment: "Creates the voting object representing a pending Proposal"
Rules about Voting:
objectId: 16
ruleOrder: 30220
type: rule
if: exists(type=="move" & subtype=="vote" & propID==%p &
from==%f & objectId==%o) &
exists(type=="player" & nickname==%f) &
exists(type="voteInProgress" & propId==%b) &
!exists(type=="vote" & propID==%p & from==%f)
then: set(objectId==%o)(type=="vote" & credited==F & cleanup==F)
comment: "One Player, one unchangable vote per proposal"
objectId: 17
ruleOrder: 30230
type: rule
if: exists(type=="vote" & from==%f & credited==F & objectId==%vo) &
exists(type=="player" & nickname==%f & score==%s & objectId==%po)
then: set(objectId==%po)(score==1+%s) &
set(objectId==%vo)(credited==T)
comment: "One Point when you vote"
The End of the Voting Period:
objectId: 18
ruleOrder: 30320
type: rule
if: exists(type=="voteInProgress" & timeGE(expiryTime) &
objectId==%o)
then: set(%o)(type="voteCompleted")
objectId: 19
ruleOrder: 30330
type: rule
if: exists(type=="voteCompleted" & propId==%p) &
exists(type=="vote" & propId==%p & from==%f & vote==%vc &
noted!=T & objectId==%vo) &
exists(type=="playerList" & listContent==%pl)
then: set(objectId==%vo)(noted==T) &
send(%pl,"Player" %f "receives one point for voting" %vc "on" %p)
comment: "Announce content of votes"
Tallying Votes:
objectId: 20
ruleOrder: 30420
type: rule
if: exists(type=="voteCompleted" & objectId==%o & propId==%p &
objectId==%o) &
count(type=="vote" & propId==%p)<(count(type=="player")/4) &
exists(type=="playerList" & listContent==%pl)
then: set(objectId==%o)(type="proposalFailed" & cleanup==T) &
send(%pl,"Proposal" %p "fails quorum.")
objectId: 21
ruleOrder: 30430
type: rule
if: exists(type=="voteCompleted" & objectId==%o & propId==%p &
objectId==%o) &
count(type=="vote" & propId==%p & vote=="FOR")>
count(type=="vote" & propId==%p & vote=="AGAINST") &
exists(type=="playerList" & listContent==%pl)
then: set(objectId==%o)(type="proposalPassed" & credited==F) &
send(%pl,"Proposal" %p "passes.")
objectId: 22
ruleOrder: 30440
type: rule
if: exists(type=="voteCompleted" & objectId==%o & propId==%p &
objectId==%o) &
count(type=="vote" & propId==%p & vote=="FOR")<=
count(type=="vote" & propId==%p & vote=="AGAINST") &
exists(type=="playerList" & listContent==%pl)
then: set(objectId==%o)(type="proposalFailed" & cleanup==T) &
send(%pl,"Proposal" %p "fails.")
objectId: 23
ruleOrder: 30450
type: rule
if: exists(type="proposalPassed" & from==%f & credited==F &
objectId==%pro) &
exists(type=="player" & nickname==%f & score==%s & objectId==%plo) &
exists(type=="playerList" & listContent==%pl)
then: set(objectId==%pro)(credited==T) &
set(objectId==%plo)(score==5+%s) &
send(%pl,%f "receives 5 points for the proposal ("5+%s")")
comment: "Five points when your proposal passes."
Implementing passed rulechanges:
objectId: 24
ruleOrder: 30520
type: rule
if: exists(type="proposalPasssed" & propId==%b) &
exists(type="changeBeingVotedOn" & batchId==%b & objectId==%o)
then: set(objectId==%o)(type=="changePassed")
objectId: 25
ruleOrder: 30530
type: rule
if: exists(type=="changePassed" & ruleChangeType=="repeal" &
target==%t & objectId==%o)
then: delete(objectId==%t) &
set(%o)(type=="changeDone" & cleanup==T)
objectId: 26
ruleOrder: 30540
type: rule
if: exists(type=="changePassed" & ruleChangeType=="amend" &
target==%t & newif==%ni & newthen==%nt & neworder==%no &
objectId==%o)
then: set(objectId==%o)(if==%ni & then==%nt & ruleOrder==&no) &
set(%o)(type=="changeDone" & cleanup==T)
objectId: 27
ruleOrder: 30550
type: rule
if: exists(type=="changePassed" & ruleChangeType=="create" &
newif==%ni & newthen==%nt & neworder==%no &
objectId==%o)
then: create(type=="rule" & if==%ni & then==%nt & ruleOrder==&no) &
set(%o)(type=="changeDone" & cleanup==T)
Cleanup Rules:
objectId: 28
ruleOrder: 99910
type: rule
if: exists(type=="proposalFailed" & propId==%p) &
exists(type=="changeBeingVotedOn" & batchId==%p & objectId==%o)
then: delete(objectId==%o)
objectId: 29
ruleOrder: 99920
type: rule
if: exists(type=="proposalFailed" & propId==%p) &
exists(type=="vote" & batchId==%p & objectId==%o)
then: delete(objecId==%o)
objectId: 30
ruleOrder: 99930
type: rule
if: exists(type=="proposalPassed" & propId==%p) &
exists(type=="vote" & batchId==%p & objectId==%o)
then: delete(objectId==%o)
objectId: 31
ruleOrder: 99940
type: rule
if: exists(type=="move" & processed!=T & objectId==%o &
moveSender==%ms)
then: sendObject(%ms)(objectId==%o)("This move didn't do anything") &
delete(objectId==%o)
comment: "If an unprocessed move is hanging around, delete it,
and send a warning to the sender."
objectId: 32
ruleOrder: 99980
type: rule
if: exists(cleanup==T & objectId==%o)
then: delete(objectId==%o)
comment: "the Final Sweeper"
objectId: 33
ruleOrder: 99990
type: rule
if: T
then: sendNow()
comment: "Send out accumulated mailings" </[email protected]>