diff --git a/text/protocols/.gitignore b/text/protocols/.gitignore new file mode 100644 index 000000000..bee8a64b7 --- /dev/null +++ b/text/protocols/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/text/protocols/README.md b/text/protocols/README.md new file mode 100644 index 000000000..3995fa085 --- /dev/null +++ b/text/protocols/README.md @@ -0,0 +1,299 @@ +[![moved to github.com/hyperledger/aries-rfcs repo](https://i.ibb.co/5jqzvN5/Screen-Shot-2019-05-21-at-2-07-33-PM.png)](https://github.com/hyperledger/aries-rfcs/blob/master/concepts/0003-protocols/README.md) + +New location: [aries-rfcs/concepts/0003-protocols](https://github.com/hyperledger/aries-rfcs/blob/master/concepts/0003-protocols/README.md) + +# 00??: Protocols + +- Name: protocols +- Authors: Daniel Hardman +- Start Date: 2018-12-28 +- PR: https://github.com/hyperledger/indy-hipe/pull/69 + +## Summary + +Defines protocols (and the closely related concept of message families) +in the context of agent-to-agent interactions, +and shows how they should be designed and documented. + +## Motivation + +When we began exploring agent-to-agent interactions, we imagined that +interoperability would be achieved by formally defining message families. +We have since learned that message family definitions must define more +than simply the attributes that are a part of each message. We also need +to formally define the roles in an interaction, the possible states those roles +can have, the way state changes in response to messages, and the errors +that may arise. + +[![protocol](protocol.png)](https://docs.google.com/presentation/d/15UAkh_2WfDk7wlto7pSL7YU9NJr_XVMgGAOeNIRbzK8/edit#slide=id.p) + +In addition, we realized that we need clear examples of how to define all +these things, so designs are consistent and robust. + +## Tutorial + +#### What is a protocol? + +A __protocol__ is a recipe for a stateful interaction. Protocols are all +around us, and are so ordinary that we take them for granted. Each of the +following interactions is stateful, and has conventions that constitute +a sort of "recipe": + +* Ordering food at a restaurant +* Buying a house +* Playing a game of chess, checkers, tic-tac-toe, etc. +* Bidding on an item in an online auction. +* Going through security at the airport when we fly +* Applying for a loan + +Protocols are a major concern for SSI agents. Agents need a recipe for +stateful interactions like: + +* Connecting with one another +* Requesting and issuing credentials +* Proving things using credentials +* Putting things in escrow (and taking them out again) +* Paying +* Reporting errors +* Negotiating +* Cooperative debugging + +#### Decentralized + +As used in the agent/DIDComm world, protocols are _decentralized_. This means +there is not an overseer for the protocol, guaranteeing information flow, +enforcing behaviors, and ensuring a coherent view. It is a subtle but +important divergence from API-centric approaches, where a server holds +state against which all other parties (clients) operate. Instead, all +parties are peers, and they interact by mutual consent and with a (hopefully) +shared understanding of the rules and goals. Protocols are like a dance--not one +that's choreographed or directed, but one where the parties make dynamic +decisions and and react to them. + +![dance](dance.jpg) + +#### Types of Protocols + +The most common protocol style in DID Communication is __request-response__. +This style involve two *parties*, with the `requester` making the first move, +and the `responder` completing the interaction. The [Protocol Discovery Protocol]( +https://github.com/hyperledger/indy-hipe/pull/73) uses this style. + +![request-response](request-response.png) + +A second common pattern that's also important is __notification__. This style also +involves two parties, but it is one-way: the `notifier` emits a message, +and the protocol ends when the `notified` receives it. The [ACK Protocol]( +https://github.com/hyperledger/indy-hipe/pull/77) and the [Problem Report +Protocol](https://github.com/hyperledger/indy-hipe/pull/65) use this style. + +![notification](notification.png) + +However, more complex protocols exist. The [Introductions Protocol]( +https://github.com/hyperledger/indy-hipe/pull/110) involves three parties, +not two. When the [Connection Management Protocol]( +https://github.com/hyperledger/indy-hipe/pull/104) includes organizations, it +may involve dozens of *participants*, and it has cycles and other complex +state evolution. + +>See [this note](parties-roles-participants.md) for definitions of the terms +"party", "role", and "participant". + +#### Agent Design + +Protocols are *the* key unit of interoperable extensibility in agents. To add a +new interoperable feature to an agent, give it the ability to handle a +new protocol. + +When agents receive messages, they map the messages to a __protocol handler__ +and possibly to an __interaction state__ that was previous persisted. The +protocol handler is code that knows the rules of a particular protocol; the +interaction state tracks progress through an interaction. For more information, +see the [agents explainer HIPE]( +https://github.com/hyperledger/indy-hipe/blob/36913b80/text/0002-agents/README.md#general-patterns) +and the [DIDComm explainer HIPE]( +https://github.com/hyperledger/indy-hipe/blob/b0708395/text/0003-did-comm/README.md). + +#### Composable + +Protocols are *composable*--meaning that you can nest one inside another. +The protocol for asking someone to repeat their last sentence can occur +inside the protocol for ordering food at a restaurant. The protocols for +reporting an error or arranging payment can occur inside a protocol for +issuing credentials. + +When we invoke one protocol inside another, we call the inner protocol a +__subprotocol__, and the outer protocol a __superprotocol__. A given protocol +may be a subprotocol in some contexts, and a standalone protocol in others. +In some contexts, a protocol may be a subprotocol from one perspective, and +a superprotocol from another (as when protocols are nested at least 3 deep). + +![super- and subprotocols](super-sub.png) + +Commonly, protocols wait for subprotocols to complete, and then they continue. +A good example of this is [ACKs](https://github.com/hyperledger/indy-hipe/blob/518b5a9a/text/acks/README.md), +which are often used as a discrete step in a larger flow. + +In other cases, a subprotocol is not "contained" inside its superprotocol. +Rather, the superprotocol triggers the subprotocol, then continues in parallel, +without waiting for the subprotocol to complete. In the [introduction protocol]( +https://github.com/hyperledger/indy-hipe/blob/790987b9/text/introductions/README.md), +the final step is to begin a connection protocol between the two introducees-- +but [the introduction superprotocol completes when the connection subprotocol +*starts*, not when it *completes*]( +https://github.com/hyperledger/indy-hipe/blob/790987b9/text/introductions/README.md#goal). + +![super- and async subprotocols](super-sub-async.png) + +#### Message Families + +A message family is a collection of messages that share a common theme, goal, or +usage pattern. The messages used by a protocol may be a subset of a particular +message family; for example, the [connection establishment protocol]( +../0031-connection-protocol/README.md) uses one subset of the messages in the +`connections` message family, and the [connection management protocol]( +https://github.com/hyperledger/indy-hipe/blob/baa1ead5/text/conn-mgmt-protocols/README.md) +uses a different subset. + +Collectively, the message types of a protocol serve as its _interface_. Each protocol +has a primary message family, and the name of the protocol is often the name of the +primary message family. + +#### Ingredients + +A protocol has the following ingredients: + +* [_Name_](template.md#name-and-version-under-tutorial) and [semver-compatible + version](semver.md) +* [_URI_ that uniquely identifies it](uris.md) +* [_Messages (primary message family)_](template.md#messages-under-tutorial) +* [_Adopted messages_](template.md#adopted-messages) +* [_Roles_](template.md#roles-under-tutorial) +* [_State_ and _sequencing rules_](template.md#state-under-tutorial) +* _Events that can change state_ -- notably, _messages_, but also _errors_, +_timeouts_, and other things +* _Constraints that provide trust and incentives_ + + +#### How to define a protocol or message family + +To define a protocol, write a HIPE. Specific instructions for +protocol HIPEs, and a discussion about the theory behind detailed +protocol concepts, are given in the [Template for Protocol HIPEs]( +template.md). The [tictactoe 1.0 protocol]( +tictactoe-1.0/README.md) is also attached to this HIPE as an example. + +[![tictactoe protocol](tictactoe-1.0/tile.png)](tictactoe-1.0/README.md) + +## Reference + +* [Message Type and Protocol Identifier URIs](uris.md) +* [Semver Rules for Protocols](semver.md) +* [State Details and State Machines](state-details.md) +* [Parties, Roles, and Participants](parties-roles-participants.md) + +## Drawbacks + +This HIPE creates some formalism around defining protocols. It doesn't go +nearly as far as SOAP or CORBA/COM did, but it is slightly more demanding +of a protocol author than the familiar world of RESTful [Swagger/OpenAPI]( +https://swagger.io/docs/specification/about/). + +The extra complexity is justified by the greater demands that agent-to-agent +communications place on the protocol definition. (See notes in [Prior Art](#prior-art) +section for details.) + +## Rationale and alternatives + +Some of the simplest DIDComm protocols could be specified in a Swagger/OpenAPI +style. This would give some nice tooling. However, not all fit into that +mold. It may be desirable to create conversion tools that allow Swagger +interop. + +## Prior art + +#### BPMN + +[BPMN](https://en.wikipedia.org/wiki/Business_Process_Model_and_Notation) is a +graphical language for modeling flows of all types (plus things less like +our protocols as well). BPMN is a mature standard sponsored by [OMG]( +https://en.wikipedia.org/wiki/Object_Management_Group). It has a nice +[tool ecosystem](https://camunda.com/bpmn/tool/). It also has an XML file +format, so the visual diagrams have a two-way transformation to and from +formal written language. And it has a code generation mode, where BPMN +can be used to drive executable behavior if diagrams are sufficiently +detailed and sufficiently standard. (Since BPMN supports various extensions +and is often used at various levels of formality, execution is not its most +common application.) + +BPMN began with a focus on centralized processes +(those driven by a business entity), with diagrams organized around the goal +of the point-of-view entity and what they experience in the interaction. This +is somewhat different from a DIDComm protocol where any given entity may experience +the goal and the scope of interaction differently; the state machine for a +home inspector in the "buy a home" protocol is _quite_ different, and somewhat +separable, from the state machine of the buyer, and that of the title insurance +company. + +BPMN 2.0 introduced the notion of a [choreography]( +https://www.visual-paradigm.com/guide/bpmn/bpmn-orchestration-vs-choreography-vs-collaboration/#bpmn-choreography), +which is much closer to the concept of an A2A protocol, and which has quite +an elegent and intuitive visual representation. However, even a BPMN +choreography doesn't have a way to discuss interactions with decorators, +adoption of generic messages, and other A2A-specific concerns. Thus, we may +lean on BPMN for some diagramming tasks, but it is not a substitute for the +HIPE definition procedure described here. + +#### WSDL + +[WSDL](https://www.w3.org/TR/2001/NOTE-wsdl-20010315) is a web-centric +evolution of earlier, RPC-style interface definition languages like +[IDL in all its varieties](https://en.wikipedia.org/wiki/Interface_description_language) +and [CORBA](https://en.wikipedia.org/wiki/Common_Object_Request_Broker_Architecture). +These technologies describe a *called* interface, but they don't describe +the caller, and they lack a formalism for capturing state changes, especiall +by the caller. They are also out of favor in the programmer community at +present, as being too heavy, [too fragile]( +https://codecraft.co/2008/07/29/decoupling-interfaces-as-versions-evolve-part-1/), +or poorly supported by current tools. + +#### Swagger / OpenAPI + +[Swagger / OpenAPI](https://swagger.io/docs/specification/about/) overlaps + about 60% with the concerns of protocol definition in agent-to-agent + interactions. We like the tools and the convenience of the paradigm + offered by OpenAPI, but where these two do not overlap, we have impedance. + + Agent-to-agent protocols must support more than 2 roles, or + two roles that are peers, whereas RESTful web services assume just client + and server--and only the server has a documented API. + + Agent-to-agent protocols are fundamentally asynchronous, + whereas RESTful web services mostly assume synchronous request~response. + + Agent-to-agent protocols have complex considerations for diffuse trust, + whereas RESTful web services centralize trust in the web server. + + Agent-to-agent protocols need to support transports beyond HTTP, whereas + RESTful web services do not. + + Agent-to-agent protocols are nestable, while + RESTful web services don't provide any special support for that construct. + +#### Other + +* [Pdef (Protocol Definition Language)](https://github.com/pdef/pdef): An alternative to Swagger. +* [JSON RPC](https://www.jsonrpc.org/specification): Defines how invocation of + remote methods can be accomplished by passing JSON messages. However, the + RPC paradigm assumes request/response pairs, and does not provide a way + to describe state and roles carefully. +* [IPC Protocol Definition Language (IPDL)](https://developer.mozilla.org/en-US/docs/Mozilla/IPDL): + This is much closer to agent protocols in terms of its scope of concerns + than OpenAPI. However, it is C++ only, and intended for use within browser + plugins. + +# Unresolved questions + +- Should we write a Swagger translator? +- If not swagger, what formal definition format should we use in the future? + diff --git a/text/protocols/adoption.png b/text/protocols/adoption.png new file mode 100644 index 000000000..d1e67c5e0 Binary files /dev/null and b/text/protocols/adoption.png differ diff --git a/text/protocols/dance.jpg b/text/protocols/dance.jpg new file mode 100644 index 000000000..ca6a38ad5 Binary files /dev/null and b/text/protocols/dance.jpg differ diff --git a/text/protocols/mturi-structure.png b/text/protocols/mturi-structure.png new file mode 100644 index 000000000..7a6c3d13f Binary files /dev/null and b/text/protocols/mturi-structure.png differ diff --git a/text/protocols/notification.png b/text/protocols/notification.png new file mode 100644 index 000000000..4927ad596 Binary files /dev/null and b/text/protocols/notification.png differ diff --git a/text/protocols/parties-roles-participants.md b/text/protocols/parties-roles-participants.md new file mode 100644 index 000000000..39d235314 --- /dev/null +++ b/text/protocols/parties-roles-participants.md @@ -0,0 +1,102 @@ +# Parties, Roles, Participants, and Drivers + +## Parties + +The __parties__ to a protocol are the entities directly responsible for achieving the protocol's goals. +When a protocol is high-level, parties are typically people or organizations; as protocols become lower-level, +parties may be specific agents tasked with detail work through delegation. + +Imagine a situation where Alice wants a vacation. She engages with a travel agent named Bob. Together, they +begin an "arrange a vacation" protocol. Alice is responsible for expressing her parameters and proving her willingness to +pay; Bob is responsible for running a bunch of subprotocols to work out the details. Alice and Bob--not software +agents they use--are parties to this high-level protocol, since they share responsibility for its goals. + +As soon as Alice has provided enough direction and hangs up the phone, Bob begins a sub-protocol with a hotel to book +a room for Alice. This sub-protocol has related but different goals--it is about booking a particular hotel room, not +about the vacation as a whole. We can see the difference when we consider that Bob could abandon the booking and choose +a different hotel entirely, without affecting the overarching "arrange a vacation" protocol. + +With the change in goal, the parties have now changed, too. Bob and a hotel concierge are the ones responsible +for making the "book a hotel room" protocol progress. Alice is an approver and indirect stakeholder, but she is +not doing the work. (In [RACI terms](https://en.wikipedia.org/wiki/Responsibility_assignment_matrix), +Alice is an "accountable" or "approving" entity, but only Bob and the concierge are "responsible" parties.) + +Now, as part of the hotel reservation, Bob tells the concierge that the guest would like access to a waverunner +to play in the ocean on day 2. The concierge engages in a sub-sub-protocol to reserve the waverunner. The +goal of this sub-sub-protocol is to reserve the equipment, not to book a hotel or arrange a vacation. The parties to this +sub-sub-protocol are the concierge and the person or automated system that manages waverunners. + +Often, parties are known at the start of a protocol; however, that is not a requirement. Some protocols might commence +with some parties not yet known or assigned. + +For many protocols, there are only two parties, and they are in a pairwise relationship. Other protocols +are more complex. Introductions involves three; an auction may involve many. + +Normally, the parties that are involved in a protocol also participate in the interaction but this is not always the +case. Consider a gossip protocol, two parties may be talking about a third party. In this case, the third party would +not even know that the protocol was happening and would definitely not participate. + +## Roles + +The __roles__ in a protocol are the perspectives (responsibilities, privileges) that parties take on an +interaction. + +This perspective is manifested in three general ways: + + * by the expectations that a party takes on in a protocol (ex. a role may be expected to do something to start a protocol). + * by the messages that a party can and does use in the course of the protocol (some messages may be reserved for a single role, while other may used by some if not all roles). + * by the state and the transition rules + +Like parties, roles are normally known at the start of the protocol but this is not a requirement. + +In an auction protocol, there are only two roles--*auctioneer* +and *bidder*--even though there may be many parties involved. + +## Participants + +The __participants__ in a protocol are the agents that consume and/or emit +[plaintext application-level messages]( +https://github.com/hyperledger/indy-hipe/tree/master/text/0026-agent-file-format#agent-plaintext-messages-ap) +that embody the protocol's interaction. Alice, Bob, and +Carol may each have a cloud agent, a laptop, and a phone; if they engage in an +introduction protocol using phones, then the agents on their phones are the participants. +If the phones talk directly over Bluetooth, this is particularly clear--but even if the +phones leverage push notifications and HTTP such that cloud agents help with routing, +only the phone agents are participants, because only they maintain state for the +interaction underway. (The cloud agents would be __facilitators__, and the laptops would +be __bystanders__). When a protocol is complete, the participant agents know about the +outcome; they may need to synchronize or replicate their state before other agents of the +parties are aware. + +## Drivers + +The __drivers__ in a protocol are entities that make decisions. They may or may not be direct parties. + +Imagine a remote chess game between Bob and Carol, conducted with software agents. The chess protocol isn't +technically about how to select a wise chess move; it's about communicating the moves so parties achieve +the shared goal of running a game to completion. Yet choices about moves are clearly made as the protocol +unfolds. These choices are made by drivers--Bob and Carol--while the agents responsible for the work of +moving the game forward wait with the protocol suspended. + +In this case, Bob and Carol could be analyzed as parties to the protocol, as well as drivers. But in other +cases, the concepts are distinct. For example, in a protocol to issue credentials, the issuing institution +and the + +in sync. + . Bob and Carol use software agents to play the game; +these agents exchange messages that express the moves. These agents are tasked with the work of moving +the game forward--but they are not tasked with winning the game. That task falls to Bob and Carol, +who are drivers. Each time a move is made, the agent must consult the driver to get a decision about +a counter-move. + +If Bob connects his software agent to an AI, then the AI becomes the driver, not Bob. + + + + +They would have the responsibility of moving the game forward quickly and accurately. However, + +However, the agents are not trying to win they game; their goal is to faithfully move the game +forwardThis is the actual work +of making the protocol progress. However, the software agents must consult their owners to learn what move +should come next. \ No newline at end of file diff --git a/text/protocols/protocol.png b/text/protocols/protocol.png new file mode 100644 index 000000000..acc5c2c37 Binary files /dev/null and b/text/protocols/protocol.png differ diff --git a/text/protocols/request-response.png b/text/protocols/request-response.png new file mode 100644 index 000000000..ec75b7886 Binary files /dev/null and b/text/protocols/request-response.png differ diff --git a/text/protocols/semver.md b/text/protocols/semver.md new file mode 100644 index 000000000..93e3813a9 --- /dev/null +++ b/text/protocols/semver.md @@ -0,0 +1,94 @@ +# Semver Rules for Protocols + +[Semver](http://semver.org) rules apply in cascading fashion to versions +of protocols and individual message types. The version of a message type +or protocol is expressed in the `semver` portion of its [identifying URI]( +uris.md). + +Individual message types are versioned as part of a coherent protocol, which +constitutes a [public API in the semver sense](https://semver.org/#spec-item-1). +An individual message type can add new optional fields, or deprecate +existing fields, [with only a change to its protocol's minor +version](https://semver.org/#spec-item-7). +Similarly, a protocol can add new message types (or [adopted +ones](#adopted-messages)) with only a change +to the minor version. It can announce deprecated fields. It can add additional +semantics around optional decorators. These are all backwards-compatible +changes, also requiring only a minor version update. + +Changes that remove fields or message types, that make formerly optional +things required, or that alter the state machine in incompatible +ways, must result in an [increase of the major version of the protocol/primary +message family](https://semver.org/#spec-item-8). + +Because protocol handling choices depend mainly on major and minor version +numbers, protocol versions are often simplified to major.minor. However, +more complex versions do have defined behavior, and should be handled +correctly by agents. + +### Version Negotiation + +The semver portion of a [message type or protocol identifier URI](uris.md) is +used to establish the version of a protocol that parties use during an +interaction. + +#### Initiator + +Unless Alice's agent (the initiator of a protocol) knows from prior history +that it should do something different, it should begin a protocol using the +highest version number that it supports. For example, if A.1 +supports versions 2.0 through 2.2 of protocol X, it should use 2.2 as the +version in the message type of its first message. + +#### Recipient Rules + +Agents for Bob (the recipient) should reject messages from protocols with major +versions different from those they support. For major version 0, they should also +reject protocols with minor versions they don't support, since semver stipulates +that [features are not stable before 1.0](https://semver.org/#spec-item-4). For +example, if B.1 supports only versions 2.0 and 2.1 of protocol X, it should reject +any messages from version 3 or version 1 or 0. In most cases, rejecting a message +means sending a `problem-report` that the message is unsupported. The `code` field +in such messages should be `version-not-supported`. Agents that receive such a +`problem-report` can then use the [Protocol Discovery Protocol]( +https://github.com/hyperledger/indy-hipe/pull/73) +to resolve version problems. + +Recipient agents should accept messages that differ from their own supported version +of a protocol only in the patch, prerelease, and/or build fields, whether these +differences make the message earlier or later than the version the recipient prefers. +These messages will be robustly compatible. + +For major version >= 1, recipients should also accept messages that differ only in that +the message's minor version is earlier than their own preference. In such a case, the +recipient should degrade gracefully to use the earlier version of the protocol. If the +earlier version lacks important features, the recipient may optionally choose to send, +in addition to a response, a `problem-report` with code `version-with-degraded-features`. + +If a recipient supports protocol X version 1.0, it should tentatively +accept messages with later minor versions (e.g., 1.2). Message types that +differ in only in minor version are guaranteed to be compatible *for the +feature set of the earlier version*. That is, a 1.0-capable agent can support +1.0 features using a 1.2 message, though of course it will lose any features +that 1.2 added. Thus, accepting such a message could have two possible outcomes: + +1. The message at version 1.2 might look and behave exactly like it did at version +1.0, in which case the message will process without any trouble. + +2. The message might contain some fields that are unrecognized and need to be ignored. + +In case 2, it is best practice for the recipient to send a `problem-report` that +is a *warning*, not an *error*, announcing that some fields could not be processed +(code = `fields-ignored-due-to-version-mismatch`). Such a message is *in addition +to* any response that the protocol demands of the recipient. + +If the recipient of a protocol's initial message generates a response, the response +should use the latest major.minor protocol version that both parties support and +know about. Generally, all messages after the first use only major.minor + +[![version negotiation matrix](version-negotiation-matrix.png)]( +https://docs.google.com/spreadsheets/d/1W5KYOqCCqmTeU4Z7XZQH9_6_0TeP5Vf5TtsOZmioyB0/edit#gid=0) + + + + diff --git a/text/protocols/state-details.md b/text/protocols/state-details.md new file mode 100644 index 000000000..c8273b89f --- /dev/null +++ b/text/protocols/state-details.md @@ -0,0 +1,102 @@ +# State Details and State Machines + +Some protocols have only one sequence of states to manage. But in many +protocols, different roles perceive the interaction differently. Each +perspective needs to be described with care. This rigor has been neglected +in many early efforts at protocol definition, and its omission is a big +miss. Analyzing all possible states and events for all roles leads to +robustness; skipping the analysis leads to fragility. + +### State machines + +By convention, our community describes state and sequence rules using the +concept of state machines, and we encourage developers who implement +protocols to build them that way. + +Many developers will have encountered a formal definition of this concept as +they wrote parsers or worked on other highly demanding tasks, and may worry +that state machines are heavy and intimidating. But as they are used in +DIDComm protocols, state machines are straightforward and elegant. They +cleanly encapsulate logic that would otherwise be a bunch of conditionals +scattered throughout agent code. Without a state machine: + +```python +if received_message.type == X and state == Y: + do_something() +... +if received_message.type == X and state in [Y, Z]: + do_something_else() + state = V +... +if event == A and state in [V, W, Z]: + state = Y + do_yet_another_thing() +``` + +With a state machine: + +```python +state_machine.handle(message) # hooks do_something() +... +state_machine.handle(message) # hooks do_something_else() +... +state_machine.handle(message) # hooks do_yet_another_thing() +``` + +Under maintenance, the robustness of the state machine approach is compelling. +Compare the approaches for how many places you might have to change if you had +a bug in how state Y is handled... The state machine approach also provides a +convenient way to load persisted state later, when a message arrives for an +interaction that is only partly complete. And it makes formal +testing for completeness and security much easier. + +The tictactoe example includes a complete state machine in less than 50 +lines of code. See [state-machine.py](tictactoe-1.0/state_machine.py). The +unit tests prove once and for all that the rules of the tictactoe protocol +(not the rules of the game, but the rules of how moves are communicated +and reacted to) are perfectly followed, in a [similarly brief chunk of code]( +tictactoe-1.0/test_state_machine.py). + +[![state machine thumbnail](state-machine-thumbnail.png)]( +tictactoe-1.0/state_machine.py) + +For an extended discussion of how state machines can be used, including in nested +protocols, and with hooks that let custom processing happen at each point in +a flow, see [https://github.com/dhh1128/distributed-state-machine]( +https://github.com/dhh1128/distributed-state-machine/blob/master/README.md). + +### Processing Points + +A protocol definition describes key points in the flow where business logic +can attach. Some of these __processing points__ are obvious, because the +protocol calls for a decision to be made. Others are implicit. Some examples +include: + +* The _beginning_ and _end_. +* The _launch of a subprotocol_. +* The _completion of a subprotocol_, or the _subprotocol changing state_. +* _Sending a message_. (For each send, the sender could choose to go silent +and abandon the interaction instead, though many +protocols would ask for notification to be emitted as best practice.) +* (Receiving a message_. (Requires validation, then integration +with policy and processes internal to the agent and its sovereign domain, +to move the interaction forward.) + +When a protocol is modeled with a state machine, each of these processing +points can be hooked without cluttering the state machine itself. This is +a nice encapsulation pattern. + +### State Machine Scope + +If you study the state machine example in the tictactoe protocol, you will +see that the state machine only concerns itself with *interacting* correctly; +it doesn't deal with implementing game logic such as deciding what a valid move +is. This is the job of an entirely different object, [game.py](tictactoe-1.0/game.py). + +Agents building protocol support are going to see a similar pattern. The state +machine guarantees that rules about interacting with other parties are followed +correctly; other logic is needed to implement business rules, make decisions, +and so forth. Notice how the tictactoe state machine includes hooks (its `pre` +and `post` and `on_error` members) that allow it to be connected to other +code at processing points. + diff --git a/text/protocols/state-machine-thumbnail.png b/text/protocols/state-machine-thumbnail.png new file mode 100644 index 000000000..bcf9eb8ba Binary files /dev/null and b/text/protocols/state-machine-thumbnail.png differ diff --git a/text/protocols/super-sub-async.png b/text/protocols/super-sub-async.png new file mode 100644 index 000000000..c626f36af Binary files /dev/null and b/text/protocols/super-sub-async.png differ diff --git a/text/protocols/super-sub.png b/text/protocols/super-sub.png new file mode 100644 index 000000000..c59cbe797 Binary files /dev/null and b/text/protocols/super-sub.png differ diff --git a/text/protocols/template-sections.png b/text/protocols/template-sections.png new file mode 100644 index 000000000..50033dc26 Binary files /dev/null and b/text/protocols/template-sections.png differ diff --git a/text/protocols/template.md b/text/protocols/template.md new file mode 100644 index 000000000..e888ac5ca --- /dev/null +++ b/text/protocols/template.md @@ -0,0 +1,211 @@ +# Template for Protocol HIPEs + +A protocol HIPE conforms to general HIPE patterns, but includes some +specific substructure. + +[![template sections](template-sections.png)](https://docs.google.com/presentation/d/15UAkh_2WfDk7wlto7pSL7YU9NJr_XVMgGAOeNIRbzK8/edit#slide=id.g5609c67f13_0_113) + +### "Name and Version" under "Tutorial" + +The first section of a protocol HIPE, under "Tutorial", should be called +"Name and Version". It should specify the official name of the protocol +and its version. + +Protocol names are conventionally lower_snake_case (especially in URIs), +but are compared case-insensitively and ignoring punctuation. This means +that all of the following protocol names are considered identical in +comparison, and can be used interchangeably, depending on what's appropriate +for a given context (a user-friendly doc vs. CSS vs. python class vs. java class): + +* `Let's Do Lunch!` +* `lets-do-lunch` +* `lets_do_lunch` +* `LetsDoLunch` + +##### URI representation + +Message types and protocols are identified with special URIs that match +certain conventions. See [Message Type and Protocol Identifier URIs]( +uris.md) for more details. + +##### Semver + +The version of a protocol is declared carefully. See [Semver Rules for +Protocols](semver.md) for details. + +### "Key Concepts" under "Tutorial" + +This is the first subsection under "Tutorial". It is short--a paragraph or +two. It defines terms and describes the flow of the interaction at a very +high level. Key preconditions should be noted (e.g., "You can't issue a +credential until you have completed the _connection_ protocol first"), as +well as ways the protocol can start and end, and what can go wrong. The +section might also talk about timing constraints and other assumptions. +After reading this section, a developer should know what problem your +protocol solves, and should have a rough idea of how the protocol works in +its simpler variants. + +### "Roles" under "Tutorial" + +>See [this note](parties-roles-participants.md) for definitions of the terms +"party", "role", and "participant". + +The "Roles" subsection comes next in a protocol HIPE. It gives a formal name +to each role in the protocol, says who and how many can play each role, and +describes constraints associated with those roles (e.g., "You can only issue +a credential if you have a DID on the public ledger"). The issue of qualification +for roles can also be explored (e.g., "The holder of the credential must be known +to the issuer"). + +The formal names for each role are important because they are used when +[agents discover one another's capabilities]( +https://github.com/hyperledger/indy-hipe/pull/73); an agent doesn't +just claim that it supports a protocol; it makes a claim about which +*roles* in the protocol it supports. An agent that supports credential +issuance and an agent that supports credential holding may have very +different features, but they both use the _credential-issuance_ protocol. +By convention, role names use lower-kebab-case but are compared +case-insensitively and ignoring punctuation. + +### "States" under "Tutorial" + +This section lists the possible states that exist for each role. It also +enumerates the events (often but not always messages) that can occur, +including errors, and what should happen to state as a result. A formal +representation of this information is provided in a _state machine matrix_. +It lists events as columns, and states as rows; a cell answers the +question, "If I am in state X (=row), and event Y (=column) occurs, +what happens to my state?" The [Tic Tac Toe example](tictactoe-1.0/README.md#states) +is typical. + +[Choreography Diagrams]( +https://www.visual-paradigm.com/guide/bpmn/bpmn-orchestration-vs-choreography-vs-collaboration/#bpmn-choreography) +from [BPMN](#bpmn) are good artifacts here, as are [PUML sequence diagrams]( +http://plantuml.com/sequence-diagram) and [UML-style state machine diagrams](http://agilemodeling.com/artifacts/stateMachineDiagram.htm). +The matrix form is nice because it forces an exhaustive analysis of every +possible event. The diagram styles are often simpler to create and consume, +and the PUML and BPMN forms have the virtue that they can support line-by-line +diffs when checked in with source code. However, they don't offer an +easy way to see if all possible flows have been considered; what they may +NOT describe isn't obvious. This--and the freedom from fancy tools--is why +the matrix form is used in many early HIPEs. We leave it up to +the community to settle on whether it wants to strongly recommend specific +diagram types. + +The formal names for each state are important, as they are used in [`ack`s]( https://github.com/hyperledger/indy-hipe/pull/77) +and [`problem-report`s](https://github.com/hyperledger/indy-hipe/pull/65)). +For example, a `problem-report` message declares which state the sender +arrived at because of the problem. This helps other participants +to react to errors with confidence. Formal state names are also used in the +agent test suite, in log messages, and so forth. + +By convention, state names use lower-kebab-case but are compared +case-insensitively and ignoring punctuation. + +State management in protocols is a deep topic. For more information, please +see [State Details and State Machines](state-details.md). + +### "Messages" under "Tutorial" + +If there is a message family associated with this protocol, this +section describes each member of it. It should also note the names and +versions of messages from other message families that are used by the +protocol (e.g., an [`ack`]( https://github.com/hyperledger/indy-hipe/pull/77) +or a [`problem-report`](https://github.com/hyperledger/indy-hipe/pull/65)). +Typically this section is written as a narrative, showing each message +type in the context of an end-to-end sample interaction. All possible +fields may not appear; an exhaustive catalog is saved for the "Reference" +section. + +Sample messages that are presented in the narrative should also be checked +in next to the markdown of the HIPE, in [DIDComm Plaintext format]( +https://github.com/hyperledger/indy-hipe/blob/master/text/0026-agent-file-format/README.md#agent-plaintext-messages-ap). + +##### Adopted Messages + +Many protocols should use general-purpose messages such as [`ack`]( +https://github.com/hyperledger/indy-hipe/pull/77) and [`problem-report`]( +https://github.com/hyperledger/indy-hipe/pull/65)) at certain points in +an interaction. This reuse is strongly encouraged because it helps us avoid +defining redundant message types--and the code to handle them--over and +over again (see [DRY principle](https://en.wikipedia.org/wiki/Don't_repeat_yourself)). + +However, using messages with generic values of `@type` (e.g., `"@type": +"did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/notification/1.0/ack"`) +introduces a challenge for agents as they route messages to their internal +routines for handling. We expect internal handlers to be organized around +protocols, since a protocol is a discrete unit of business value as well +as a unit of testing in our agent test suite. Early work on agents has +gravitated towards pluggable, routable protocols as a unit of code +encapsulation and dependency as well. Thus the natural routing question +inside an agent, when it sees a message, is "Which protocol handler should +I route this message to, based on its @type?" A generic `ack` can't be +routed this way. + +Therefore, we allow a protocol to __adopt__ messages into its namespace. +This works very much like python's `from module import symbol` syntax. +It changes the `@type` attribute of the adopted message. Suppose a `rendezvous` +protocol is identified by the URI `did:sov:mYhnRbzCbsjhiQzDAstHgU;spec/rendezvous/2.0`, +and its definition announces that it has adopted generic 1.x `ack` +messages. When such `ack` messages are sent, the `@type` should now use +the alias defined inside the namespace of the `rendezvous` protocol: + +![diff on @type caused by adoption](adoption.png) + +Adoption should be declared in an "Adopted" subsection of "Messages" in +a protocol HIPE. When adoption is specified, it should include a __minimum +adopted version__ of the adopted message type: "This protocol adopts +`ack` with version >= 1.4". All versions of the adopted message that share +the same major number should be compatible, given the [semver rules](#semver-rules) +noted above. + +### "Constraints" under "Tutorial" + +Many protocols have constraints that help parties build trust. +For example, in buying a house, the protocol includes such things as +commission paid to realtors to guarantee their incentives, title insurance, +earnest money, and a phase of the process where a home inspection takes +place. If you are documenting a protocol that has attributes like +these, explain them here. If not, the section can be omitted. + +### "Messages" under "Reference" + +Unless the "Messages" section under "Tutorial" covered everything that +needs to be known about all message fields, this is where the data type, +validation rules, and semantics of each field in each message type are +details. Enumerating possible values, or providing ABNF or regexes is +encouraged. Following conventions such as [those for date- +and time-related fields](https://github.com/hyperledger/indy-hipe/pull/76) +can save a lot of time here. + +Each message type should be associated with one or more roles in the +protocol. That is, it should be clear which roles can send and receive +which message types. + +### "Examples" under "Reference" + +This section is optional. It can be used to show alternate flows through +the protocol. + +### "Collateral" under "Reference" + +This section is optional. It could be used to reference files, code, +relevant standards, oracles, test suites, or other artifacts that would +be useful to an implementer. In general, collateral should be checked in +with the HIPE. + +### "Localization" under "Reference" + +If communication in the protocol involves humans, then localization of +message content may be relevant. Default settings for localization of +all messages in the protocol can be specified in an `l10n.json` file +described here and checked in with the HIPE. See ["Decorators at Message +Type Scope"](https://github.com/hyperledger/indy-hipe/blob/318f265d508a3ddf1da7d91c79ae4ae27ab9142b/text/localized-messages/README.md#decorator-at-message-type-scope) +in the [Localization HIPE](https://github.com/hyperledger/indy-hipe/pull/64). + +### "Message Catalog" under "Reference" + +If the protocol has a formally defined catalog of codes (e.g., for errors +or for statuses), define them in this section. See ["Message Codes and +Catalogs"](https://github.com/hyperledger/indy-hipe/blob/318f265d508a3ddf1da7d91c79ae4ae27ab9142b/text/localized-messages/README.md#message-codes-and-catalogs) +in the [Localization HIPE](https://github.com/hyperledger/indy-hipe/pull/64). diff --git a/text/protocols/tictactoe-1.0/README.md b/text/protocols/tictactoe-1.0/README.md new file mode 100644 index 000000000..bf15f2341 --- /dev/null +++ b/text/protocols/tictactoe-1.0/README.md @@ -0,0 +1,303 @@ +# Tic Tac Toe 1.0 Protocol + +![tictactoe by Symode09 via Wikimedia Commons](banner.png) + +## Summary + +Describes a simple protocol, already familiar to most +developers, as a way to demonstrate how all protocols should +be documented. + +## Motivation + +Playing tic-tac-toe is a good way to test whether agents are +working properly, since it requires two parties to take turns +and to communicate reliably about state. However, it is also +pretty simple, and it has a low bar for trust (it's not dangerous +to play tic-tac-toe with a malicious stranger). Thus, we expect +agent tic-tac-toe to be a good way to test basic plumbing and to +identify functional gaps. The game also provides a way of testing +interactions with the human owners of agents, or of hooking up +an agent AI. + +## Tutorial + +[Tic-tac-toe is a simple game](https://en.wikipedia.org/wiki/Tic-tac-toe) +where players take turns placing Xs and Os in a 3x3 grid, attempting to +capture 3 cells of the grid in a straight line. + +### Name and Version + +This defines the `tictactoe` protocol, version 1.x, as identified by the +following URI: + + did:sov:SLfEi9esrjzybysFxQZbfq;spec/tictactoe/1.0 + +### Key Concepts + +A tic-tac-toe game is an interaction where 2 parties take turns to +make up to 9 moves. It starts when either party proposes the game, and +ends when one of the parties wins, or when all all cells in the grid +are occupied but nobody has won (a draw). + +Illegal moves and moving out of turn are errors that trigger a complaint +from the other player. However, they do not scuttle the interaction. +A game can also be abandoned in an unfinished state by either player, +for any reason. Games can last any amount of time. + + About the Key Concepts section: Here we describe the flow at a very + high level. We identify preconditions, ways the protocol can start + and end, and what can go wrong. We also talk about timing + constraints and other assumptions. + +### Roles + +There are two parties in a tic-tac-toe game, but only one role, `player`. +One player places 'X' for the duration of a game; the other places 'O'. +There are no special requirements about who can be a player. The parties do +not need to be trusted or even known to one another, either at the outset or +as the game proceeds. No prior setup is required, other than an ability to +communicate. + + About the Roles section: Here we name the roles in the protocol, + say who and how many can play each role, and describe constraints. + We also explore qualifications for roles. + +### States + +The states of each `player` in the protocol evolve according to the +following state machine: + +[![state machine](player-state-machine.png)](https://docs.google.com/spreadsheets/d/1hwbJ_sgaobQlNinMQlRhE52EsAxh07rmBKl-yKq_0qI/edit?usp=sharing) + +When a player is in the `my-move` state, possible valid events include +`send move` (the normal case), `send outcome` (if the player decides +to abandon the game), and `receive outcome` (if the other player +decides to abandon). A `receive move` event could conceivably occur, too-- +but it would be an error on the part of the other player, and would +trigger a `problem-report` message as described above, leaving the +state unchanged. + +In the `receive-move` state, `send move` is an impossible event for a +properly behaving player. All 3 of the other events could occur, causing +a state transition. + +In the `wrap-up` state, the game is over, but communication with the +outcome message has not yet occurred. The logical flow is `send outcome`, +whereupon the player transitions to the `done` state. + + About the States section: Here we explain which states exist for each + role. We also enumerate the events that can occur, including messages, + errors, or events triggered by surrounding context, and what should + happen to state as a result. In this protocol, we only have one role, + and thus only one state machine matrix. But in many protocols, each + role may have a different state machine. + +### Messages + +All messages in this protocol are part of the "tictactoe 1.0" message +family uniquely identified by this DID reference: `did:sov:SLfEi9esrjzybysFxQZbfq;spec/tictactoe/1.0` + + About the "DID Reference" URI that appears here: DIDs can be resolved + to a DID doc that contains an endpoint, to which everything after a + semicolon can be appended. Thus, if this DID is publicly registered + and its DID doc gives an endpoint of http://example.com, this URI + would mean that anyone can find a formal definition of the protocol at + http://example.com/spec/tictactoe/1.0. It is also possible to use a + traditional URI here, such as http://example.com/spec/tictactoe/1.0. + If that sort of URI is used, it is best practice for it to reference + immutable content, as with a link to specific commit on github: + https://github.com/hyperledger/indy-hipe/blob/4a17a845/text/protocols/tictactoe-1.0/README.md#messages + +##### `move` message + +The protocol begins when one party sends a `move` message +to the other. It looks like this: + +[![move 1](move-1.png)](move-1.json) + +`@id` is required here, as it establishes a [message thread](https://github.com/hyperledger/indy-hipe/pull/30) +that will govern the rest of the game. + +`me` tells which mark (X or O) the sender is placing. +It is required. + +`moves` is optional in the first message of the interaction. If missing +or empty, the sender of the first message is inviting the recipient to +make the first move. If it contains a move, the sender is moving first. + +Moves are strings like "X:B2" that match the regular expression `(?i)[XO]:[A-C][1-3]`. +They identify a mark to be placed ("X" or "O") and a position in the 3x3 +grid. The grid's columns and rows are numbered like familiar spreadsheets, +with columns A, B, and C, and rows 1, 2, and 3. + +`comment` is optional and probably not used much, but could be a way +for players to razz one another or chat as they play. It follows the +conventions of [localized messages]( +https://github.com/hyperledger/indy-hipe/pull/64). + +Other decorators could be placed on tic-tac-toe messages, such as those +to enable [message timing](https://github.com/hyperledger/indy-hipe/blob/2167762c31dec10777a36d14c5038130b1a06670/text/message-timing/README.md#decorators) +to force players to make a move within a certain period of time. + +##### Subsequent Moves +Once the initial `move` message has been sent, game play continues +by each player taking turns sending responses, which are also `move` +messages. With each new message the `move` array inside the message +grows by one, ensuring that the players agree on the current accumulated +state of the game. The `me` field is still required and must +accurately reflect the role of the message sender; it thus alternates +values between `X` and `O`. + +Subsequent messages in the game use the [message threading]( +https://github.com/hyperledger/indy-hipe/pull/30) mechanism where the +`@id` of the first `move` becomes the `~thread.thid` for the duration +of the game. + +An evolving sequence of `move` messages might thus look like this, +suppressing all fields except what's required: + +##### Message/Move 2 + +[![move 2](move-2.png)](move-2.json) + +This is the first message in the thread that's sent by the `player` placing +"O"; hence it has `myindex` = 0. + +##### Message/Move 3 + +[![move 3](move-3.png)](move-3.json) + +This is the second message in the thread by the player placing "X"; hence +it has `myindex` = 1. + +##### Message/Move 4 + +[![move 4](move-4.png)](move-4.json) + +...and so forth. + +Note that the order of the items in the `moves` array is NOT significant. +The state of the game at any given point of time is fully captured by +the moves, regardless of the order in which they were made. + +If a player makes an illegal move or another error occurs, the other +player can complain using a [problem-report]( +https://github.com/hyperledger/indy-hipe/blob/6a5e4fe2d7e14953cd8e3aed07d886176332e696/text/error-handling/README.md#the-problem-report-message-type) +message, with `explain.@l10n.code` set to one of the values defined +in the Message Catalog section (see below). + +##### `outcome` message +Game play ends when one player sends a `move` message that manages to +mark 3 cells in a row. Thereupon, it is best practice, but not strictly +required, for the other player to send an acknowledgement in the form +of an `outcome` message. + +[![outcome](outcome.png)](outcome.json) + +The `moves` and `me` fields from a `move` message can also, optionally, +be included to further document state. The `winner` field is required. +Its value may be "X", "O", or--in the case of a draw--"none". + +This `outcome` message can also be used to document an abandoned game, +in which case `winner` is `null`, and `comment` can be used to +explain why (e.g., timeout, loss of interest). + + About the Messages section: Here we explain the message types, but + also which roles send which messages, what sequencing rules apply, + and how errors may occur during the flow. The message begins with + an announcement of the identifier and version of the message + family, and also enumerates error codes to be used with problem + reports. This protocol is simple enough that we document the + datatypes and validation rules for fields inline in the narrative; + in more complex protocols, we'd move that text into the Reference + > Messages section instead. + +### Constraints + +Players do not have to trust one another. Messages do not have to be +authcrypted, although anoncrypted messages still have to have a +path back to the sender to be useful. + + About the Constraints section: Many protocols have rules + or mechanisms that help parties build trust. For example, in buying + a house, the protocol includes such things as commission paid to + realtors to guarantee their incentives, title insurance, earnest + money, and a phase of the process where a home inspection takes + place. If you are documenting a protocol that has attributes like + these, explain them here. + +## Reference + + About the Reference section: If the Tutorial > Messages section + suppresses details, we would add a Messages section here to + exhaustively describe each field. We could also include an + Examples section to show variations on the main flow. + +### Collateral + +A reference implementation of the logic of a game is provided with this +HIPE as python 3.x code. See [game.py](game.py). There is also a simple +hand-coded AI that can play the game when plugged into an agent (see +[ai.py](ai.py)), and a set of unit tests that prove correctness (see +[test_tictactoe.py](test_tictactoe.py)). + +A full implementation of the state machine is provided as well; see +[state_machine.py](state_machine.py) and [test_state_machine.py]( +test_state_machine.py). + +The game can be played interactively by running `python game.py`. + +### Localization + +The only localizable field in this message family is `comment` on both `move` +and `outcome` messages. It contains ad hoc text supplied by the sender, +instead of a value selected from an enumeration and identified by `code` for +use with message catalogs. This means the only approach to localize `move` or +`outcome` messages is to submit `comment` fields to an automated translation +service. Because the locale of `tictactoe` messages is not predefined, each +message must be decorated with `~l10n.locale` to make automated translation +possible. + +There is one other way that localization is relevant to this protocol: in error +messages. Errors are communicated through the general [problem-report]( +https://github.com/hyperledger/indy-hipe/blob/6a5e4fe2d7e14953cd8e3aed07d886176332e696/text/error-handling/README.md#the-problem-report-message-type) +message type rather than through a special message type that's part of the +`tictactoe` family. However, we define a catalog of tictactoe-specific error codes +below to make this protocol's specific error strings localizable. + +Thus, all instances of this message family carry localization metadata +in the form of an implicit `~l10n` decorator that looks like this: + +[![~l10n](~l10n.png)](~l10n.json) + +This JSON fragment is checked in next to the narrative content of this +HIPE as [~l10n.json](~l10n.json), for easy machine parsing. + +Individual messages can use the `~l10n` decorator to supplement or override +these settings. + +For more information about localization concepts, see the [HIPE about localized +messages](https://github.com/hyperledger/indy-hipe/blob/569357c6/text/localized-messages/README.md#message-codes-and-catalogs). + +### Message Catalog + +To facilitate localization of error messages, all instances of this message +family assume the following catalog in their `~l10n` data: + +[![catalog](catalog.png)](catalog.json) + +When referencing this catalog, please be sure you have the correct version. The +official, immutable URL to this version of the catalog file is: + + https://github.com/hyperledger/indy-hipe/blob/fc7a6028/text/tictactoe-protocol/catalog.json + +This JSON fragment is checked in next to the narrative content of this HIPE +as [catalog.json](catalog.json), for easy machine parsing. The catalog +currently contains localized alternatives only for English. Other language +contributions would be welcome. + +For more information, see the [Message Catalog section of the localization HIPE]( +https://github.com/hyperledger/indy-hipe/blob/569357c6/text/localized-messages/README.md#message-codes-and-catalogs +). + diff --git a/text/protocols/tictactoe-1.0/ai.py b/text/protocols/tictactoe-1.0/ai.py new file mode 100644 index 000000000..6469de082 --- /dev/null +++ b/text/protocols/tictactoe-1.0/ai.py @@ -0,0 +1,85 @@ +import game +import random + +def winnable_in_n_moves(line:list, cells, player:str): + # Tells how many moves would get 3-in-a-row on a given + # line, for a given player. Returns None if player + # can't win, or 0 if player has already won. + n = 3 + for i in line: + cell = cells[i] + if cell == player: + n -= 1 + elif cell is None: + pass + else: + return None + return n + +def next_move(g:game.Game, player:str, randomize=True): + ''' + Return the best cell for the player to pick, given current + game state--or None if the game has already been won. + ''' + player = player.upper() + turn = g.whose_turn() + if turn and (player != turn): + raise Exception("It isn't player %s's turn." % player) + cells = g.cells + # First, figure out which line each player is closest + # to winning on. If someone already *has* won, then + # return None. + bests = ([4,None],[4,None]) + candidate_lines = game.LINES + if randomize: + candidate_lines = random.shuffle(candidate_lines[0:]) + for line in game.LINES: + ns = [ + winnable_in_n_moves(line, cells, player), + winnable_in_n_moves(line, cells, game.other_player(player)) + ] + for i in [0,1]: + n = ns[i] + # If someone already won, there is no next move. + if n == 0: + return None + # If this player can't win with this line, then it + # can't be the basis for one of their best choices. + if n is None: + continue + # If this line is closer to winning, or if it's + # equally close to winning compared to a previously + # seen line, but this one includes the center square + # but the previous best does not, pick this line. + if n < bests[i][0] or \ + ((4 in line) and (n == bests[i][0]) and + (bests[i][1] is None or (4 not in bests[i][1]))): + bests[i][0] = n + bests[i][1] = line + # First, prefer something on my best line that blocks + # my opponent on their best line. + choices = None + if bests[0][1] and bests[1][1]: + choices = [x for x in bests[0][1] if x in bests[1][1]] + # If I can't do that, then see who's winning. If me, + # play offense by staying one step ahead (moving toward + # my win). If them, play defense by blocking them. + if not choices: + if bests[0][0] <= bests[1][0]: + line = bests[0][1] + else: + line = bests[1][1] + # If we get here and line is None, then there is no + # way to win the game. Just pick a random cell that's + # left. + if not line: + choices = [x for x in range(9) if cells[x] is None] + else: + choices = [line[x] for x in [0,1,2] if cells[line[x]] is None] + # If the center cell is one of our choices, always pick it. + # Otherwise, return first of equally good alternatives. + if 4 in choices: + choice = 4 + else: + choice = random.choice(choices) if randomize else choices[0] + return game.idx_to_key(choice) diff --git a/text/protocols/tictactoe-1.0/banner.png b/text/protocols/tictactoe-1.0/banner.png new file mode 100644 index 000000000..aecba2479 Binary files /dev/null and b/text/protocols/tictactoe-1.0/banner.png differ diff --git a/text/protocols/tictactoe-1.0/catalog.json b/text/protocols/tictactoe-1.0/catalog.json new file mode 100644 index 000000000..e67acf158 --- /dev/null +++ b/text/protocols/tictactoe-1.0/catalog.json @@ -0,0 +1,13 @@ + +{ + "not-your-turn": { + "en": "You moved when it was my turn." + }, + "already-occupied": { + "en": "You can't place a mark in cell {where}. It is already occupied." + }, + "bad-move": { + "en": "Move \"{move}\" makes no sense. Expected a string like \"X:A1\" or \"O:C3\"." + } +} + diff --git a/text/protocols/tictactoe-1.0/catalog.png b/text/protocols/tictactoe-1.0/catalog.png new file mode 100644 index 000000000..f4248ee6a Binary files /dev/null and b/text/protocols/tictactoe-1.0/catalog.png differ diff --git a/text/protocols/tictactoe-1.0/game.py b/text/protocols/tictactoe-1.0/game.py new file mode 100644 index 000000000..acc78c66e --- /dev/null +++ b/text/protocols/tictactoe-1.0/game.py @@ -0,0 +1,170 @@ +import re + +_move_split_pat = re.compile(r'\s*,\s*|\s*') + + +def key_to_idx(key): + bad_key = (not isinstance(key, str)) or len(key) != 2 + if not bad_key: + c = key[0].upper() + if c not in 'ABC': + bad_key = True + try: + r = int(key[1]) + if r < 1 or r > 3: + bad_key = True + except: + bad_key = True + if bad_key: + raise KeyError('Bad key "%s". Expected A1 through C3.' % key) + return ((r - 1) * 3) + 'ABC'.index(c) + + +def idx_to_key(i): + if (not isinstance(i, int)) or i < 0 or i > 8: + raise ValueError('Expected idx between 0 and 8, inclusive.') + return 'ABC'[i % 3] + str(int(1 + (i / 3))) + + +def other_player(player:str): + if (not isinstance(player, str)) or (len(player) != 1) or (player not in 'xoXO'): + raise ValueError('Expected X or O.') + if player in 'xX': + return 'O' + return 'X' + + +ROW1 = [0, 1, 2] +ROW2 = [3, 4, 5] +ROW3 = [6, 7, 8] +COL1 = [0, 3, 6] +COL2 = [1, 4, 7] +COL3 = [2, 5, 8] +DIAG1 = [0, 4, 8] +DIAG2 = [2, 4, 6] +LINES = [ROW1, ROW2, ROW3, COL1, COL2, COL3, DIAG1, DIAG2] + + +class Game: + def __init__(self): + self.cells = [None]*9 + self.first = None + + def __getitem__(self, key): + return self.cells[key_to_idx(key)] + + def __setitem__(self, key, x_or_o): + if (not isinstance(x_or_o, str)) or (len(x_or_o) != 1) or (x_or_o not in 'xoXO') : + raise ValueError('Bad value. Expected X or O.') + x_or_o = x_or_o.upper() + if not self.first: + self.first = x_or_o + else: + whose_turn = self.whose_turn() + if x_or_o != whose_turn: + raise ValueError("Bad value. Expected %s, since it that player's turn." % whose_turn) + i = key_to_idx(key) + if self.cells[i] is not None: + raise Exception("Can't reuse square %s. It already has an %s in it." % (x_or_o, self.cells[i])) + self.cells[i] = x_or_o + + def whose_turn(self): + if self.first: + xs = self.cells.count('X') + os = self.cells.count('O') + if xs > os: + return 'O' + elif os > xs: + return 'X' + return self.first + + def __str__(self): + s = ' A B C\n' + for row in range(3): + s += str(row + 1) + for col in range(3): + c = self.cells[row * 3 + col] + s += ' ' + if c is None: c = '-' + s += c + if row != 2: + s += '\n' + return s + + def winner(self): + """ + Search for a winner. Return None if game isn't over, "X" if player X won, + "O" if player O won, and the empty string if it's a draw. + """ + draw = True + for line in LINES: + c1 = self.cells[line[0]] + if c1: + c2 = self.cells[line[1]] + c3 = self.cells[line[2]] + if c1 == c2 and c1 == c3: + return c1 + if (not c2) or (not c3): + draw = False + else: + draw = False + if draw: + return '' + + def is_done(self): + return self.winner() is not None + + def load(self, moves): + """Load an array of moves like "X:A1" into a game.""" + for m in moves: + self[m[2:]] = m[0] + + def dump(self): + """Convert a game into an array of moves like "X:A1".""" + moves = [] + for i in range(9): + c = self.cells[i] + if c: + moves.append('%s:%s' % (c, idx_to_key(i))) + return moves + + +if __name__ == '__main__': + # Play an interactive game with an AI. + + import random + import sys + + import ai + try: + while True: + print("\nYou be Xs, I'll be Os.") + g = Game() + player = random.choice('XO') + if player == 'O': + print("I'll go first.") + w = None + for i in range(9): + if player == 'O': + choice = ai.next_move(g, player) + print('My move: %s' % choice) + g[choice] = player + else: + print(str(g)) + sys.stdout.write('Your move: ') + x = input().strip() + g[x] = 'X' + w = g.winner() + if w: + break + player = other_player(player) + print(str(g)) + if w == 'X': + print('You win!') + elif w == 'O': + print('I win.') + else: + print('Draw.') + except KeyboardInterrupt: + print() + sys.exit(0) \ No newline at end of file diff --git a/text/protocols/tictactoe-1.0/handler.py b/text/protocols/tictactoe-1.0/handler.py new file mode 100644 index 000000000..ab21c0194 --- /dev/null +++ b/text/protocols/tictactoe-1.0/handler.py @@ -0,0 +1,44 @@ +import re + +import game +import ai + +MOVE_MSG_TYPE = 'did:sov:SLfEi9esrjzybysFxQZbfq;spec/tictactoe/1.0/move' +RESULTS_MSG_TYPE = 'did:sov:SLfEi9esrjzybysFxQZbfq;spec/tictactoe/1.0/results' + +TYPES = [ + MOVE_MSG_TYPE, + RESULTS_MSG_TYPE +] + +def load_game(moves): + + +def handle(wc, agent): + try: + t = wc.obj['@type'] + if t == MOVE_MSG_TYPE: + them = wc.obj.get('ill_be', '') + if them and isinstance(them, str) and len(them) == 1 and them in 'XO': + them = them.strip().upper() + else: + raise Exception('Expected "ill_be" to contain either "X" or "O".') + moves = wc.obj.get('moves', []) + if not isinstance(moves, list) or len(moves) > 9: + raise Exception('Expected "moves" to be a list of at most 9 items.') + g = game.Game() + g.load(moves) + w = g.winner() + if w: + agent.trans.send('{"@type": "result", "outcome": "%s won."}') + me = game.other_player(them) + if g.whose_turn() + + if them == 'X': + g.load(wc.obj['moves']) + choice = ai.next_move(g, me) + g[choice] = me + agent.trans.send('{"@type": "%s"}' % MOVE_MSG_TYPE, wc.sender) + except Exception as e: + agent.trans.send('{"@type": "problem-report", "explain_ltxt": "%s"}', wc.sender) + return True \ No newline at end of file diff --git a/text/protocols/tictactoe-1.0/move-1.json b/text/protocols/tictactoe-1.0/move-1.json new file mode 100644 index 000000000..dabc716c4 --- /dev/null +++ b/text/protocols/tictactoe-1.0/move-1.json @@ -0,0 +1,9 @@ + +{ + "@type": "did:sov:SLfEi9esrjzybysFxQZbfq;spec/tictactoe/1.0/move", + "@id": "518be002-de8e-456e-b3d5-8fe472477a86", + "me": "X", + "moves": ["X:B2"], + "comment": "Let's play tic-tac-toe. I'll be X. I pick cell B2." +} + diff --git a/text/protocols/tictactoe-1.0/move-1.png b/text/protocols/tictactoe-1.0/move-1.png new file mode 100644 index 000000000..b165a5041 Binary files /dev/null and b/text/protocols/tictactoe-1.0/move-1.png differ diff --git a/text/protocols/tictactoe-1.0/move-2.json b/text/protocols/tictactoe-1.0/move-2.json new file mode 100644 index 000000000..8b1724d0f --- /dev/null +++ b/text/protocols/tictactoe-1.0/move-2.json @@ -0,0 +1,8 @@ + +{ + "@type": "did:sov:SLfEi9esrjzybysFxQZbfq;spec/tictactoe/1.0/move", + "~thread": { "thid": "518be002-de8e-456e-b3d5-8fe472477a86", "sender_order": 0 }, + "moves": ["X:B2", "O:A1"], + "me": "O" +} + diff --git a/text/protocols/tictactoe-1.0/move-2.png b/text/protocols/tictactoe-1.0/move-2.png new file mode 100644 index 000000000..7bc25a5de Binary files /dev/null and b/text/protocols/tictactoe-1.0/move-2.png differ diff --git a/text/protocols/tictactoe-1.0/move-3.json b/text/protocols/tictactoe-1.0/move-3.json new file mode 100644 index 000000000..26ba7f8c7 --- /dev/null +++ b/text/protocols/tictactoe-1.0/move-3.json @@ -0,0 +1,8 @@ + +{ + "@type": "did:sov:SLfEi9esrjzybysFxQZbfq;spec/tictactoe/1.0/move", + "~thread": { "thid": "518be002-de8e-456e-b3d5-8fe472477a86", "sender_order": 1 }, + "moves": ["X:B2", "O:A1", "X:A2"], + "me": "X" +} + diff --git a/text/protocols/tictactoe-1.0/move-3.png b/text/protocols/tictactoe-1.0/move-3.png new file mode 100644 index 000000000..231341a71 Binary files /dev/null and b/text/protocols/tictactoe-1.0/move-3.png differ diff --git a/text/protocols/tictactoe-1.0/move-4.json b/text/protocols/tictactoe-1.0/move-4.json new file mode 100644 index 000000000..f743ca691 --- /dev/null +++ b/text/protocols/tictactoe-1.0/move-4.json @@ -0,0 +1,8 @@ + +{ + "@type": "ddid:sov:SLfEi9esrjzybysFxQZbfq;spec/tictactoe/1.0/move", + "~thread": { "thid": "518be002-de8e-456e-b3d5-8fe472477a86", "sender_order": 1 }, + "moves": ["X:B2", "O:A1", "X:A2", "O:B1"], + "me": "O" +} + diff --git a/text/protocols/tictactoe-1.0/move-4.png b/text/protocols/tictactoe-1.0/move-4.png new file mode 100644 index 000000000..eb9b3d6c4 Binary files /dev/null and b/text/protocols/tictactoe-1.0/move-4.png differ diff --git a/text/protocols/tictactoe-1.0/outcome.json b/text/protocols/tictactoe-1.0/outcome.json new file mode 100644 index 000000000..123b0ade0 --- /dev/null +++ b/text/protocols/tictactoe-1.0/outcome.json @@ -0,0 +1,8 @@ + +{ + "@type": "did:sov:SLfEi9esrjzybysFxQZbfq;spec/tictactoe/1.0/outcome", + "@thread": { "thid": "518be002-de8e-456e-b3d5-8fe472477a86", "seqnum": 3 }, + "winner": "X", + "comment": "You won!" +} + diff --git a/text/protocols/tictactoe-1.0/outcome.png b/text/protocols/tictactoe-1.0/outcome.png new file mode 100644 index 000000000..a3f2f3eaf Binary files /dev/null and b/text/protocols/tictactoe-1.0/outcome.png differ diff --git a/text/protocols/tictactoe-1.0/player-state-machine.png b/text/protocols/tictactoe-1.0/player-state-machine.png new file mode 100644 index 000000000..141d04e3d Binary files /dev/null and b/text/protocols/tictactoe-1.0/player-state-machine.png differ diff --git a/text/protocols/tictactoe-1.0/state_machine.py b/text/protocols/tictactoe-1.0/state_machine.py new file mode 100644 index 000000000..c6d0e65e4 --- /dev/null +++ b/text/protocols/tictactoe-1.0/state_machine.py @@ -0,0 +1,48 @@ +# Define states and events, including symbolic (numeric) constants and their friendly names. +STATE_NAMES = ['my-move', 'their-move', 'wrap-up', 'done'] +for i in range(len(STATE_NAMES)): + globals()[STATE_NAMES[i].replace('-', '_').upper() + '_STATE'] = i + +EVENT_NAMES = ['send move', 'receive move', 'send outcome', 'receive outcome'] +for i in range(len(EVENT_NAMES)): + globals()[EVENT_NAMES[i].upper().replace(' ', '_') + '_EVENT'] = i + + +class StateMachine: + def __init__(self, logic, pre=None, post=None, on_error=None): + self.state = None + self.logic = logic + self.pre = pre + self.post = post + self.on_error = on_error + + def handle(self, event): + s = self.state + if event == SEND_MOVE_EVENT: + if s in [None, MY_MOVE_STATE]: + self._transition_to(WRAP_UP_STATE if self.logic.is_done() else THEIR_MOVE_STATE, event) + else: + raise AssertionError(f"Programmer error; I can't move when state = {STATE_NAMES[s]}.") + elif event == RECEIVE_MOVE_EVENT: + if s in [None, THEIR_MOVE_STATE]: + self._transition_to(WRAP_UP_STATE if self.logic.is_done() else MY_MOVE_STATE, event) + else: + self._on_error(f"Other party can't move when state = {STATE_NAMES[s]}") + elif event in [SEND_OUTCOME_EVENT, RECEIVE_OUTCOME_EVENT]: + if s != DONE_STATE: + self._transition_to(DONE_STATE, event) + else: + raise AssertionError("Illegal event %d." % event) + + def _on_error(self, msg): + if self.on_error: + self.on_error(msg) + + def _transition_to(self, state, event): + if self.pre: + # Ask permission before transitioning + if not self.pre(state, event): + return + self.state = state + if self.post: + self.post(state, event) \ No newline at end of file diff --git a/text/protocols/tictactoe-1.0/test_state_machine.py b/text/protocols/tictactoe-1.0/test_state_machine.py new file mode 100644 index 000000000..334edf4b4 --- /dev/null +++ b/text/protocols/tictactoe-1.0/test_state_machine.py @@ -0,0 +1,164 @@ +import pytest + +from state_machine import * + +class NeverDone: + def is_done(self): + return False + +class BeDone: + def is_done(self): + return True + +@pytest.fixture +def sm(): + return StateMachine(NeverDone()) + +def test_no_state_on_start(sm): + assert sm.state is None + +def test_my_move_first(sm): + sm.handle(SEND_MOVE_EVENT) + assert sm.state == THEIR_MOVE_STATE + +def test_their_move_first(sm): + sm.handle(RECEIVE_MOVE_EVENT) + assert sm.state == MY_MOVE_STATE + +def test_illegal_move_by_me(sm): + sm.handle(SEND_MOVE_EVENT) + # Sending a move when it's the other person's turn + # is our programmer error, so it should assert. + with pytest.raises(AssertionError): + sm.handle(SEND_MOVE_EVENT) + +def test_wrapup_from_them(sm): + sm.state = THEIR_MOVE_STATE + sm.logic = BeDone() + sm.handle(RECEIVE_MOVE_EVENT) + assert sm.state == WRAP_UP_STATE + sm.handle(SEND_OUTCOME_EVENT) + assert sm.state == DONE_STATE + +def test_wrapup_from_me(sm): + sm.state = MY_MOVE_STATE + sm.logic = BeDone() + sm.handle(SEND_MOVE_EVENT) + assert sm.state == WRAP_UP_STATE + sm.handle(SEND_OUTCOME_EVENT) + assert sm.state == DONE_STATE + +def test_send_move_in_wrapup(sm): + sm.state = WRAP_UP_STATE + # Sending a move when it's time to send an outcome + # is our programmer error, so this should assert. + with pytest.raises(AssertionError): + sm.handle(SEND_MOVE_EVENT) + +def test_receive_move_in_wrapup(sm): + sm.state = WRAP_UP_STATE + # Receiving a move when it's time to send an outcome + # is a programmer error on the other side. It shouldn't + # assert in our code, but should generate an error. + # This is an error by the other party, so it should + # trigger on_error. + sm.on_error = ErrorHandler() + sm.handle(RECEIVE_MOVE_EVENT) + assert bool(sm.on_error.msg) + assert sm.state == WRAP_UP_STATE + +class ErrorHandler: + def __init__(self): + self.msg = None + + def __call__(self, msg): + self.msg = msg + +def test_illegal_move_by_them(sm): + # This is an error by the other party, so it should + # trigger on_error. + sm.on_error = ErrorHandler() + sm.handle(RECEIVE_MOVE_EVENT) + assert sm.on_error.msg is None + assert sm.state == MY_MOVE_STATE + sm.handle(RECEIVE_MOVE_EVENT) + assert bool(sm.on_error.msg) + assert sm.state == MY_MOVE_STATE + +def test_early_exit_by_me(sm): + sm.handle(RECEIVE_MOVE_EVENT) + sm.handle(SEND_OUTCOME_EVENT) + assert sm.state == DONE_STATE + +def test_early_exit_by_them(sm): + sm.handle(SEND_MOVE_EVENT) + sm.handle(SEND_OUTCOME_EVENT) + assert sm.state == DONE_STATE + +def test_unrecognized_event(sm): + with pytest.raises(AssertionError): + sm.handle(25) + +def test_event_while_done(sm): + for i in range(len(EVENT_NAMES)): + sm.state = DONE_STATE + sm.on_error = ErrorHandler() + if i == SEND_MOVE_EVENT: + with pytest.raises(AssertionError): + sm.handle(i) + assert sm.on_error.msg is None + elif i in [SEND_OUTCOME_EVENT, RECEIVE_OUTCOME_EVENT]: + # should be ignored + sm.handle(i) + assert sm.on_error.msg is None + else: #i == RECEIVE_MOVE_EVENT: + sm.handle(i) + assert bool(sm.on_error.msg) + +class HookHandler: + def __init__(self, response): + self.event = None + self.response = response + self.state = None + + def __call__(self, state, event): + self.state = state + self.event = event + return self.response + +def test_pre_hooks_allow(sm): + for i in range(DONE_STATE): + sm.state = i + sm.pre = HookHandler(True) + sm.handle(SEND_OUTCOME_EVENT) + # Because we're allowing a transition, the pre hook should + # be called with old state, but now we should have new state. + assert sm.pre.state != i + assert sm.state == sm.pre.state + assert sm.pre.event == SEND_OUTCOME_EVENT + +def test_pre_hooks_deny(sm): + for i in range(DONE_STATE): + sm.state = i + # This hook handler should refuse to allow us to transition. + sm.pre = HookHandler(False) + sm.post = HookHandler(None) + sm.handle(SEND_OUTCOME_EVENT) + # Because we're denying a transition, the pre hook should + # be called with new state, and we should still have old state. + assert sm.state == i + assert sm.pre.state != i + assert sm.pre.event == SEND_OUTCOME_EVENT + # We should never have called the post event. + assert sm.post.event == None + +def test_post_hooks(sm): + for i in range(DONE_STATE): + sm.state = i + sm.post = HookHandler(None) + sm.handle(SEND_OUTCOME_EVENT) + # Because we're allowing a transition, the post hook should + # be called with new state. + assert sm.state != i + assert sm.post.state == sm.state + assert sm.post.event == SEND_OUTCOME_EVENT diff --git a/text/protocols/tictactoe-1.0/test_tictactoe.py b/text/protocols/tictactoe-1.0/test_tictactoe.py new file mode 100644 index 000000000..4ccbd5528 --- /dev/null +++ b/text/protocols/tictactoe-1.0/test_tictactoe.py @@ -0,0 +1,144 @@ +import unittest +import sys +import os +import random + +import game +import ai + +class GameTest(unittest.TestCase): + def setUp(self): + self.game = game.Game() + def test_first(self): + self.assertEqual(None, self.game.first) + self.game['b3'] = 'x' + self.assertEqual('X', self.game.first) + def test_turns_enforced(self): + self.game['b1'] = 'o' + ok = True + try: + self.game['b2'] = 'o' + ok = False + except: + pass + if not ok: + self.fail('Expected game not to allow two turns in row by same person.') + def test_get_item_from_empty(self): + self.assertEqual(None, self.game['a1']) + def test_simple_set_item(self): + self.game['a1'] = 'x' + self.assertEqual('X', self.game['a1']) + self.game['c3'] = 'o' + self.assertEqual('O', self.game['c3']) + self.game['b2'] = 'X' + self.assertEqual('X', self.game['b2']) + self.game['a3'] = 'O' + self.assertEqual('O', self.game['a3']) + def test_set_item_with_bad_value(self): + bad_values = ['',None,'*','0',1,0] + for bad in bad_values: + self.game = game.Game() # reset state + with self.assertRaises(ValueError): + self.game['a1'] = bad + def test_bad_key(self): + bad_keys = ['1a','a4','d1','a1 ',' a1','a0','a01',11,None,'a12','aa1'] + for bad in bad_keys: + with self.assertRaises(KeyError): + self.game[bad] + def test_cant_clobber_existing_cell(self): + self.game['c3'] = 'o' + with self.assertRaises(Exception): + self.game['c3'] = 'x' + def test_winner1(self): + self.game['b2'] = 'o' + self.game['a2'] = 'x' + self.game['b1'] = 'o' + self.game['b3'] = 'x' + self.game['c3'] = 'o' + self.assertEqual(None, self.game.winner()) + def test_winner2(self): + self.game['a1'] = 'x' + self.game['b2'] = 'o' + self.game['b1'] = 'x' + self.game['a2'] = 'o' + self.game['c2'] = 'x' + self.assertEqual(None, self.game.winner()) + self.game['c3'] = 'o' + self.game['c1'] = 'x' + self.assertEqual('X', self.game.winner()) + def test_bad_idx_to_key(self): + bad_idx = [0,9,-1,'0',None,'a','a1'] + for bad in bad_idx: + with self.assertRaises(ValueError): + game.idx_to_key(bad_idx) + def test_good_idx_to_key(self): + def all_good_idx(): + for r in '123': + for c in 'ABC': + yield c + r + expected = 0 + for idx in all_good_idx(): + self.assertEqual(idx, game.idx_to_key(expected)) + expected += 1 + def test_good_other_player(self): + opposites = 'xXoO' + for player in opposites: + opposite = opposites[(opposites.index(player) + 2) % 4].upper() + self.assertEqual(opposite, game.other_player(player)) + def test_bad_other_player(self): + bad_players = ['fred',None,'',0,1,-1,'xx','xo','Ox'] + for bad in bad_players: + with self.assertRaises(ValueError): + game.other_player(bad) + def test_load_and_dump(self): + moves = 'X:B2,O:C3,X:B1,O:B3,X:A3'.split(',') + self.game.load(moves) + dumped = self.game.dump() + self.assertEqual(len(dumped), len(moves)) + for m in moves: + self.assertTrue(m in dumped) + +class AITest(unittest.TestCase): + def test_line_winnable(self): + for line in game.LINES: + cells = [None]*9 + self.assertEqual(3, ai.winnable_in_n_moves(line, cells, 'x')) + self.assertEqual(3, ai.winnable_in_n_moves(line, cells, 'o')) + cells[line[0]] = 'X' + self.assertEqual(2, ai.winnable_in_n_moves(line, cells, 'X')) + self.assertEqual(None, ai.winnable_in_n_moves(line, cells, 'O')) + cells[line[0]] = 'O' + cells[line[1]] = 'O' + self.assertEqual(None, ai.winnable_in_n_moves(line, cells, 'X')) + self.assertEqual(1, ai.winnable_in_n_moves(line, cells, 'O')) + cells[line[0]] = 'X' + cells[line[1]] = 'X' + cells[line[2]] = 'X' + self.assertEqual(0, ai.winnable_in_n_moves(line, cells, 'X')) + self.assertEqual(None, ai.winnable_in_n_moves(line, cells, 'O')) + def test_first_move(self): + g = game.Game() + self.assertEqual('B2', ai.next_move(g, 'x')) + def test_head_to_head(self): + # Make the AI play against itself a bunch of times. Randomize which + # side starts. Every game should take a full 9 moves, and every gamee + # should end with a draw. + for i in range(100): + g = game.Game() + player = random.choice('XO') + n = 0 + while True: + cell = ai.next_move(g, player) + g[cell] = player + n += 1 + w = g.winner() + if w: + if w != g.first or n != 9: + self.fail('Game won unexpectedly:\n%s.' % str(g)) + break + if n == 9: + break + player = game.other_player(player) + +if __name__ == '__main__': + unittest.main() diff --git a/text/protocols/tictactoe-1.0/tictactoe.png b/text/protocols/tictactoe-1.0/tictactoe.png new file mode 100644 index 000000000..1e1707533 Binary files /dev/null and b/text/protocols/tictactoe-1.0/tictactoe.png differ diff --git a/text/protocols/tictactoe-1.0/tile.png b/text/protocols/tictactoe-1.0/tile.png new file mode 100644 index 000000000..649e05e71 Binary files /dev/null and b/text/protocols/tictactoe-1.0/tile.png differ diff --git a/text/protocols/tictactoe-1.0/~l10n.json b/text/protocols/tictactoe-1.0/~l10n.json new file mode 100644 index 000000000..7cbeffcb0 --- /dev/null +++ b/text/protocols/tictactoe-1.0/~l10n.json @@ -0,0 +1,8 @@ +{ + "~l10n": { + "locales": [{"locale": "en", "fields": ["comment"]}], + "catalogs": [ + "https://github.com/hyperledger/indy-hipe/blob/a0d57d1f/text/protocols/catalog.json" + ] + } +} diff --git a/text/protocols/tictactoe-1.0/~l10n.png b/text/protocols/tictactoe-1.0/~l10n.png new file mode 100644 index 000000000..32456233f Binary files /dev/null and b/text/protocols/tictactoe-1.0/~l10n.png differ diff --git a/text/protocols/uris.md b/text/protocols/uris.md new file mode 100644 index 000000000..34f9f14cb --- /dev/null +++ b/text/protocols/uris.md @@ -0,0 +1,83 @@ +# Message Type and Protocol Identifier URIs + +Message types and protocols are identified with URIs that match certain +conventions. + +### MTURI + +A __message type URI__ (MTURI) identifies message types unambiguously. +Standardizing its format is important because it is parsed by agents that +will map messages to handlers--basically, code will look at this string and +say, "Do I have something that can handle this message type inside protocol +*X* version *Y*?" When that analysis happens, it must do more than compare +the string for exact equality; it may need to check for semver compatibility, +and it has to compare the protocol name and message type name ignoring case +and punctuation. + +The URI MUST be composed as follows: + +![MTURI structure](mturi-structure.png) + +```ABNF +message-type-uri = doc-uri delim protocol-name + "/" protocol-version "/" message-type-name +delim = "?" / "/" / "&" / ":" / ";" / "=" +protocol-name = identifier +protocol-version = semver +message-type-name = identifier +identifier = alpha *(*(alphanumeric / "_" / "-" / ".") alphanumeric) +``` + +It can be loosely matched and parsed with the following regex: + + (.*?)([a-z0-9._-]+)/(\d[^/]*)/([a-z0-9._-]+)$ + +A match will have captures groups of (1) = `doc-uri`, (2) = `protocol-name`, +(3) = `protocol-version`, and (4) = `message-type-name`. + +The goals of this URI are, in descending priority: + +* Code can use the URI to route messages to protocol +handlers using semver rules. + +* The definition of a protocol should be tied to the URI such +that it is semantically stable. This means that once version 1.0 +of a protocol is defined, its definition should not change in +ways that would break implementations. (See the [semver](#semver) +section for more on this.) + +* Developers can discover information about novel protocols, using +the URI to browse or search the web. + +The `doc-uri` portion is any URI that exposes documentation about +protocols. A developer should be able to browse to that URI and use human intelligence +to look up the named and versioned protocol. Optionally and preferably, the +full URI may produce a page of documentation about the specific message type, +with no human mediation involved. + +### PIURI + +A shorter URI that follows the same conventions but lacks the +`message-type-name` portion is called a __protocol identifier URI__ +(PIURI). Its loose matcher regex is: + + (.*?)([a-z0-9._-]+)/(\d[^/]*)/?$ + +Some examples of valid MTURIs and PIURIs include: + +* `http://example.com/protocols?which=lets_do_lunch/1.0/` (PIURI with fully automated lookup of protocol docs) +* `http://example.com/message_types?which=lets_do_lunch/1.0/proposal` (MTURI) +* `https://github.com/hyperledger/indy-hipe/tree/d7879f5e/text:trust_ping/1.0/ping` + (MTURI). Note that this URI returns a 404 error if followed directly--but + per rules described above, the developer should browse to the doc root + ([https://github.com/hyperledger/indy-hipe/tree/d7879f5e/text]( + https://github.com/hyperledger/indy-hipe/tree/d7879f5e/text + )) and look for documentation on the `trust_ping/1.0` protocol. +* `did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/trust_ping/1.0/ping` (MTURI) This + uses a DID reference to look up an endpoint named `spec` that serves + information about protocols. (The exact syntax of DID references--URIs + where the DID functions like a domain name, and additional info is + fetched from a DID Doc in much the same way IP address and hostname + definitions are fetched from a DNS record--is still being finalized. + See the latest [DID Spec](https://w3c-ccg.github.io/did-spec) for details.) + diff --git a/text/protocols/version-negotiation-matrix.png b/text/protocols/version-negotiation-matrix.png new file mode 100644 index 000000000..d61c3b1a5 Binary files /dev/null and b/text/protocols/version-negotiation-matrix.png differ