December 2011 - Posts
Anterior Post
En mi anterior post, comenté la nueva versión 1.1 (hay ahora una 1.2 Beta) del Windows Azure Toolkit for Social Games. Tiene juegos simples para demostrar el uso de Javascript, HTML5, procesamiento de movidas del juego, uso de Azure web roles y worker roles. Veamos de explorar en este post el armado de la lógica del juego, en Javascript, usando TDD (Test-Driven Development) y QUnit.
Hay tests en línea en:
http://watgames4.cloudapp.net/Test
Ejecutemos los test de Tic Tac Toe Game Logic:
http://watgames4.cloudapp.net/Samples/ClientTest/TicTacToeGameTest
Esta página está usando QUnit para tests en el cliente usando Javascripot. Escribí posts introductorios:
TDD with Javascript and QUnit
TDD con Javascript y QUnit
La página que visitamos está probando la lógica del juego de Ta Te Ti. Recordemos, cada juego está implementado en partes, la lógica es una de ellas:
El código cliente reside en TicTacToeGame.js dentro del proyecto SocialGames.Web. Sus primeras líneas:
TTTColor = { Empty: 0, Cross: 1, Circle: 2 };
function TicTacToeGame() {
this.board = [
[TTTColor.Empty, TTTColor.Empty, TTTColor.Empty],
[TTTColor.Empty, TTTColor.Empty, TTTColor.Empty],
[TTTColor.Empty, TTTColor.Empty, TTTColor.Empty]
];
}
TicTacToeGame.prototype.move = function (x, y, color) {
this.board[x][y] = color;
};
TicTacToeGame.prototype.isEmpty = function (x, y) {
return this.board[x][y] == TTTColor.Empty;
};
....
La vista razor (TicTacToeGameTest.cshtml) fue escrita al mismo tiempo que la lógica, usando TDD (Test-Driven Development). Veamos los primeros tests:
test("Create Empty Board", function () {
var game = new TicTacToeGame();
for (var x = 0; x < 3; x++)
for (var y = 0; y < 3; y++)
ok(game.isEmpty(x, y));
equal(game.isTie(), false);
equal(game.hasWinner(), false);
});
test("Valid Moves on Empty Board", function () {
var game = new TicTacToeGame();
for (var x = 0; x < 3; x++)
for (var y = 0; y < 3; y++) {
ok(game.isValid(x, y, TTTColor.Cross));
ok(game.isValid(x, y, TTTColor.Circle));
}
});
test("No Winner in Empty Board", function () {
var game = new TicTacToeGame();
equal(game.getWinner(), TTTColor.Empty);
});
test("Get Winner in First Row", function () {
var game = new TicTacToeGame();
game.move(0, 0, TTTColor.Cross);
game.move(1, 0, TTTColor.Cross);
game.move(2, 0, TTTColor.Cross);
equal(game.getWinner(), TTTColor.Cross);
equal(game.isTie(), false);
equal(game.hasWinner(), true);
});
La idea es avanzar de a pequeños pasos, test por test, diseñando la API de la lógica del juego, y su conducta esperada. De esta manera, vamos avanzando sin gastar tanto tiempo en prueba manual, y menos tiempo en depuración. Esto es recomendable, pero más aún cuando se trabaja con un lenguaje dinámico como Javascript. Una batería de pruebas puede salvarnos el día en caso de un refactoreo grande. Vean que el Four In A Row siguió un camino similar.
Bien, no todo puede ser testeado fácilmente, o construido usando TDD. Algunos de los servicios agnósticos del juego (es decir, independientes del juego que se implemente) estan usando Ajax y Blob Storage, y para probarlos debemos considerar el uso de Ajax asincrónico (pueden comenzar construyendo tests sincrónicos si quieren). Pueden ver:
http://watgames4.cloudapp.net/Test/ServerInterfaceTest
(Para esta página, deben ingresar usando una cuenta de Facebook o de Windows Live ID, el ejemplo usa Federated Security y Access Control Service (ACS))
Esta vez, el sistema bajo prueba es el Service Interface:
Hay algunos trucos en el código de prueba (ServerInterfaceTest.cshmlt), un fragmento:
test("Call User/Verify", function () {
var success = function (result) { ok(true); start(); };
var error = ajaxGetError;
stop(10000);
expect(1);
si.sendAjaxGet(apiURL + "user/verify", success);
});
expect es una función provista por QUnit que prepara al framework para recibir un ok(true) alguna vez durante la ejecución de la prueba. Esa confirmación se incluye en la función callback success que será llamada luego de la ejecución exitosa de la llamada asincrónica .sendAjaxGet. La vida Async no es fácil ;-)
Más análisis del código en próximos posts. Debería adaptar algún juego para que use Node.js para procesar sus jugadas.
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez
Anterior Post
Una de las primeras clases que implemente en AjLispRb es el ambiente (“environment”). Esta vez lo llamé contexto: un diccionaro donde guardar pares nombre/valor, los valores de los átomos con nombre. El código:
module AjLisp
class Context
def initialize(parent = nil)
@parent = parent
@values = Hash.new
end
def getValue(name)
if @values.has_key?(name)
return @values[name]
end
if @parent != nil
return @parent.getValue(name)
end
return nil
end
def setValue(name, value)
@values[name] = value
end
end
end
La clase fue codificada usando TDD (Test-Driven Development), los primeros tests en test/test_context.rb. Algunos tests:
def test_not_defined_is_nil
context = AjLisp::Context.new
assert_nil(context.getValue(:foo))
end
def test_set_and_get_value
context = AjLisp::Context.new
context.setValue(:foo, "bar")
assert_equal("bar", context.getValue(:foo))
end
def test_get_value_from_parent
parent = AjLisp::Context.new
parent.setValue(:foo, "bar")
context = AjLisp::Context.new(parent)
assert_equal("bar", context.getValue(:foo))
end
Inicialmente, yo usaba strings para los nombre, pero ahora estoy usando los símbolos de Ruby como :foo. Tengo un test que asegura la independencia de los valores del contexto padre y del hijo:
def test_override_value_from_parent
parent = AjLisp::Context.new
parent.setValue(:foo, "bar")
context = AjLisp::Context.new(parent)
context.setValue(:foo, "bar2")
assert_equal("bar2", context.getValue(:foo))
assert_equal("bar", parent.getValue(:foo))
end
Cada contexto puede tener un contexto padre. Hay un contexto “tope”, definido lib/ajlisp.rb:
module AjLisp
@context = Context.new
@context.setValue :quote, FPrimitiveQuote.instance
@context.setValue :first, PrimitiveFirst.instance
@context.setValue :rest, PrimitiveRest.instance
@context.setValue :cons, PrimitiveCons.instance
@context.setValue :list, PrimitiveList.instance
@context.setValue :lambda, FPrimitiveLambda.instance
@context.setValue :flambda, FPrimitiveFLambda.instance
@context.setValue :mlambda, FPrimitiveMLambda.instance
@context.setValue :let, FPrimitiveLet.instance
@context.setValue :define, FPrimitiveDefine.instance
@context.setValue :do, FPrimitiveDo.instance
@context.setValue :if, FPrimitiveIf.instance
@context.setValue :definef, FPrimitiveDefinef.instance
@context.setValue :definem, FPrimitiveDefinem.instance
@context.setValue :+, PrimitiveAdd.instance
@context.setValue :-, PrimitiveSubtract.instance
@context.setValue :*, PrimitiveMultiply.instance
@context.setValue :/, PrimitiveDivide.instance
def self.context
return @context
end
# ...
end
Los nuevos contexts son creado por varias primitivas. En el comienzo, existe el contexto tope definido arriba, como:
Si tenemos que definir una función que retorna el segundo elemento de una lista, podemos usar la primitiva define:
(define second (a) (first (rest a)))
ahora tenemos en el contexto tope una nueva entrada para second:
El contexto tope tiene ahí en esa nueva entrada el valor que representa un lambda (lambda (a) (first (rest a)) (en realidad, debería aclarar que se guarda el resultado de haber evaluado el lambda, que es un closure (clausura) pero ya llegaremos al tema en próximo post).
Cuando invocamos:
(second (quote (one two three)))
un nuevo contexto se crea, con su padre apuntando al contexto tope (de nuevo, es algo más complejo, veremos clausuras). Ese nuevo contexto tiene un par nombre/valor:
Este contexto es descartado LUEGO de esta evaluaciónThat de second (puede sobrevivir indirectamente si luego de la evaluación quedó referenciado por una clausura). Cuando definimos un valor simple:
(define one 1)
el contexto tope es modificado, para tener un nuevo par nombre/valor:
Próximos temas: primitivas y formas especiales, su invocación, clausuras, macros, e invocación de métodos nativos de Ruby.
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez
Anterior Post
En mi semana sabática hice algunos avances en mi proyecto de código abierto AjCore, una implementación simple de un repositorio de contenido escrito en C#:
https://github.com/ajlopez/AjCoRe
Si Content Repository es un nuevo concepto para Uds., pueden ver mis enlaces http://delicious.com/ajlopez/contentrepository y la introducción al tema escrita por Roy Fielding JSR-170 overview .
Los conceptos principales: hay Workspaces. Cada workspace tiene un Root Node (nodo raíz). Cada Node puede tener Child Nodes (nodos hijos), y Properties. Una Property tiene un Name y un Value:
En el diagrama de arriba, nodos y espacios de trabajo están represetnados por interfaces. La idea es tener diferentes implementaciones de esas abstracciones. En mi anterior post presenté dos implementaciones: una representando un sistema de archivos/directorios, permitiendo solamente operaciones de lecturas. Y otra implementación, más extensible, usando nodos arbitrarios en memoria. Desde entonces, agregué el soporte de grabar y recuperar nodos arbitrarios usando un almancén, un lugar de persistencia.
Ahora, el proyecto está así:
donde:
A: Es la implementación de las clases concretas de workspaces y nodes en memoria, con el nuevo soporte OPCIONAL de tener un store.
B: La implementación de solo lectura de espacio de trabajo representando un directorio, donde los nodos son más directorios y archivos.
C: El nuevo IStore (abstracto) y la primer implementación concreta, usando una jerarquía de directorios y archivos XML para persistir las propiedades de un nodo.
D: El soporte de transacciones que dura la vida de una sesión.
Veamos las nuevas capacidades de almacenamiento. La interfaz Stores.IStore:
public interface INode
{
string Name { get; }
INode Parent { get; }
PropertyList Properties { get; }
NodeList ChildNodes { get; }
string Path { get; }
}
Stores.Xml.Store es la primer implementación de esta abstracción. Uds. podrían agregar otras, por ejemplo usando JSON, una base de datos relacional, o un NoSQL. Pueden crear, remover nodos y actualizar sus propiedades, dentro de una sesión, igual que en el anterior posts. Pero ahora, el espacio de trabajo puede ser inyectado con una implementación de IStore, que comienza a usarse entonces automáticamente. Ejemplo (ver más detalle en el código de los tests):
// Reference store in a directory
Store store = new Store("c:\\myworkspace");
// Workspace using that store to retrieve root node and their descendant
// (lazy loading)
Workspace workspace = new Workspace(store, "myws");
// Session accesing that workspace
Session session = new Session(workspace);
// You can use session to get the root node
// in case you have no direct workspace reference
INode root = session.Workspace.RootNode;
// Updates are made into a transaction
using (var tr = session.OpenTransaction())
{
// Accessing a node
INode node = root.ChildNodes["father"];
// Changing a property
session.SetPropertyValue(node, "Name", "Adam");
// Creating a nodes
INode newnode = session.CreateNode(node, "newson", new Property[] {
new Property("Name", "Abel")
});
// Removing a node
session.RemoveNode(newson);
tr.Complete();
}
El tr.Complete() es el encargado de actualizar los archivos XML con las propiedades de los nodos que cambiaron. Un archivo de ejemplo:
<?xml version="1.0" encoding="utf-8"?>
<Properties>
<Name>John</Name>
<Age type="int">35</Age>
<Male type="bool">true</Male>
<Hired type="datetime">2000-01-01T00:00:00</Hired>
<Height type="double">167.2</Height>
<Salary type="decimal">120000.5</Salary>
</Properties>
El código en Transaction.Complete:
var nodestoupdate =
this.operations.Where(op => !(op is RemoveNodeOperation)).Select(op => op.Node).Distinct();
var nodestodelete =
this.operations.Where(op => op is RemoveNodeOperation).Select(op => op.Node).Distinct();
nodestoupdate = nodestoupdate.Except(nodestodelete);
foreach (var node in nodestoupdate)
this.store.SaveProperties(node.Path, node.Properties);
foreach (var node in nodestodelete)
this.store.RemoveNode(node.Path);
Podría mejorarla, tomando en cuante que algunas implementaciones de IStore podría preferir actualizar propiedades, en lugar de un nodo completo (por ejemplo, una implementación que use como almacén una base de datos).
Quiero escribir otra implementación de IStore usando JSON (debería pensar cómo guardar los tipos originales de cada propiedad). Otro trabajo pendiente: recuperar un node desde un espacio de trabajo sabiendo su path completo, y permitir hacer consultas de nodos (¿usando XPath? Estoy todavía decidiendo).
Si ven los commit en el repositorio, verán cómo fue implementado todo esto de almacenamiento usando TDD (Test-Driven Development).
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez
Post Anterior
En este post explicaré con un ejemplo simple cómo código Smalltalk puede ser compilado a Javascript usando el proyecto AjTalk.Compiler, el nombre del ejecutable es ajtalkc. El compilador está escrito desde cero en C#. La forma de compilar (la salida) fue fruto de investigar temas dinámicos de Javascript, y para Smalltalk en particular, leí los posts que mencioné en Smalltalk y Javascript sin ver la implementación interna, sólo lo publicado en post. En cada proyecto trato de ver cómo implementar lo que quiero, sin “infectarme” de otras implementaciones, simplemente para ver hasta donde puedo llegar desde las ideas básicas. Y usando TDD, la implementación interna va surgiendo a medida que se va desarrollando.
Como es usual, el código está en mi repo:
http://code.google.com/p/ajtalk/
Yo hice “fileOut” de una clase simple de Pharo: Point. El fragmento del principio:
Object subclass: #Point
instanceVariableNames: 'x y'
classVariableNames: ''
poolDictionaries: ''
category: 'Graphics-Primitives'!
!Point commentStamp: '<historical>' prior: 0!
I represent an x-y pair of numbers usually
designating a location on the screen.!
!Point methodsFor: '*Polymorph-Geometry' stamp: 'gvc 10/31/2006 11:01'!
directionToLineFrom: p1 to: p2
"Answer the direction of the line from the receiver
position.
< 0 => left (receiver to right)
= => on line
> 0 => right (receiver to left)."
^((p2 x - p1 x) * (self y - p1 y)) -
((self x - p1 x) * (p2 y - p1 y))! !
....
Lo compilé usando el programa de comando de línea ajtalkc.exe (vean el proyecto de consola AjTalk.Compiler en la solución AjTalk; tiene un directorio CodeFiles con varios fileouts):
ajtalkc CodeFiles\PharoCorePoint.st
Se crea un nuevo archivo: Program.js. Exploremos cómo una clase Smalltalk puede ser representada en Javascript. Estas líneas definen la clase Point como una función Javascript, y su clase como otra función BLOCKED SCRIPT
function PointClass()
{
}
function Point()
{
}
Nada especial. Las siguientes dos líneas son:
Point.prototype.__class = PointClass.prototype;
Point.classPrototype = PointClass.prototype;
De esta manera, cada instancia de la clase Point tiene una variable __class que apunta al singleton PointClass.prototype. Y la función/”clase” Point tiene una variable classPrototype para apuntar al mismo singleton, si es necesario.
¿Cómo creamos una nueva instancia de Point? Usando un método de PointClass, en este caso definiendo su método basicNew:
PointClass.prototype['_basicNew'] = function() { return new Point(); };
Agregué un carácter _ para evitar la colisión de nombres con cualquier método Javascript.
Ahora, ¿Cómo asegurar que todas las instancias tengan sus variables de instanca? Definiéndolas en su prototipo:
Point.prototype.x = null;
Point.prototype.y = null;
Y la parte más interesante: ¿Cómo traducir un método Javascript a Smalltalk? Sea el método min en Smalltalk:
min
"Answer a number that is the minimum
of the x and y of the receiver."
^self x min: self y! !
Su traducción a un método del prototipo de Point:
Point.prototype['_min'] = function()
{
var self = this;
return send(send(self, '_x'), '_min_', [send(self, '_y')]);
};
Noten que definí la variable self para ser usado por el resto del método (especialmente como variable libre en cualquier closure Javascript que encuentre, donde this podría apuntar a otro objeto). Y el uso de un método ayudante send que recibe: un objeto destino, el nombre del método a invocar, y un arreglo con sus argumentos.
Todo esto es trabajo en progreso. Ahora, estoy generando las líneas adicionales para usar Program.js desde Node.js (pueden ver el repo, con un ejemplo de Node.js), y también con otras líneas adicionales, usarlo desde el browser (ver el repo, ya hay un ejemplo cliente). Debería escribir diferentes compiladores para el server y el cliente, todos usando el mismo núcleo: el compilador de NodeJs ahora es una subclase del compilador básico de Javascript. Lo mismo el compilador para el browser. Tengo planeado compilar a Ruby. Todo esto es una prueba de concepto para mostrar el poder de tener un modelos abstracto del programa a ejecutar/compilar. Hasta podría usar ese modelo abstracto para ejecutar un programa, en lugar de compilar a bytecodes. O podría usar ese modelo abstracto, para compilar a bytecodes de AjTalk.
Próximos temas a visitar en posts: opciones de compilación, ejemplo para el browser, ejemplo para Node.js. Cómo acceder desde el código generado a objectos Javascript. Las funciones utilitarias. Implementación de herencia, bloques y clausuras.
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez
Es el título de la próxima charla gratuita que @woloski y yo daremos en el Microsoft User Group de Buenos Aires, Argentina. Ver detalles en
http://www.mug.org.ar/Eventos/3715.aspx
Será el próximo miércoles, 21 de Diciembre, a las 18:30hs. Tienen que visitar ese link si quieren inscribirse (hay inscripción previa).
La idea a presentar está basada en desarrollos que hicimos para:
Windows Azure Toolkit for Social Games
Ya escribí sobre Azure y Social Games (con Tankster y los nuevos ejemplos simples) en:
Social Games Programming
Programando Juegos Sociales
Para la charla visitaremos:
- HTML5, en especial su uso de Canvas para dibujar un simple tablero
- Javascript, por ejemplo Knockout para tener un ViewModel en el cliente que al cambiar refresca elementos de la página
- Uso de TDD en Javascript, con QUnit
- Socket.IO cliente para comunicarse con un servidor
- Node.js para levantar un servidor de jugadas, usando Socket.IO (quiero también escribir un ejemplo con sockets TCP)
- Elementos de Tankster (seguridad federada en Azure, etc…) aunque lo principal de la charla se basa en los nuevos ejemplos simples (Tankster puede ser una aplicación muy grande para estudiar como primer ejemplo).
Si quieren leer sobre Tankster y los ejemplos simples nuevos, les recomiendo los posts en los enlaces que mencioné arriba.
Sobre temas de HTML5, Node.js, Javascript, TDD ya tengo escritos:
HTML5: Links, News and Resources
BLOCKED SCRIPT Links, News and Resources
Node.js: Links, News and Resources
TDD With Javascript and QUnit
My posts about QUnit
Y estos son los enlaces que fui descubriendo mientras estudiaba estos temas:
http://www.delicious.com/ajlopez/javascript
http://delicious.com/ajlopez/html5
http://delicious.com/ajlopez/socketio
http://www.delicious.com/ajlopez/nodejs
Mañana Sábado Diciembre 17, el bueno de @cprieto dará una VAN (reunión virtual) en AltNet Hispano:
http://cprieto.com/2011/12/13/proxima-van-html5-para-los-no-iniciados/
El bueno de @theprogrammer ya dio una VAN sobre Node.js:
http://altnethispano.org/wiki/van-2011-08-06-nodejs.ashx
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez
Hoy, Diciembre 8, comienzo la segunda semana sabática del año. Terminará el domingo 18 de Diciembre. Entonces, escribí mi lista de tareas personal, mis compromisos para estos días. Una semana sabática es una oportunidad de hacer investigación, trabajo, y programación, más allá del trabajo diario. Es tiempo de calidad empleado para practicar y mejorar. Después de planear un poco (tiempo disponible, tiempo estimado, temas que puedo abordar) esta es la lista de lo que quiero encarar (hay una lista de temas no técnicos, también):
Escribir código, trabajando en los proyectos:
- AjCoRe, implementando un IStore.
- AjContab, esta vez una versión .NET.
- AjActors, basado en la última implementación de AjAgents.
- AjLispRb, completando primitivas, macros, proceso de archivos.
Escribir posts sobre:
- AjTalk generando AjScript (2 posts)
- Social Games (2 posts)
- AjLisp en Ruby
- AjLisp en Javascript
- Ruby Fun Day (2 posts, in Spanish)
- Mi día en RubyConfAr (2 posts, in Spanish)
- Mi día en Smalltalks 2011 (2 posts, in Spanish)
- jBPM5, Drools por JBoss (1 post)
Algunos de esos posts son parte de una serie (por ejemplo, lo de Drools puede que me lleve más de un post, pero compromiso para esta semana es escribir solo uno).
Practicar y estudiar:
- Clojure, via REPL, o con Eclipse
- Ruby
- Python
- Web Service Security (.NET)
- Java con Maven (oops ;-)
Otros:
- Asistir a la Ruby Buenos Aires Meetup
Muchas de esas actividades tendrán entregables visibles, por ejemplos, los posts terminaran siendo publicados por acá, durante Y DESPUES de esta semana (quiero publicar uno solo por día). Como es habitual el código agregado queda cada hora en los repos.
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez
Siguiente Post
Hace unos años, me topé con el proyecto Apache Jackrabbit, de código abierto que implementa la JSR170 (vean mis primeros links (2008) en http://delicious.com/ajlopez/jsr170), pero no le presté mucha atención. La semana pasada, en una lista de correo privada, el tema de repositorio de contenido apareció de nuevo, así que estuve leyendo algunos enlaces:
http://en.wikipedia.org/wiki/Content_repository
http://en.wikipedia.org/wiki/Content_repository_API_for_Java
http://www.jcp.org/en/jsr/detail?id=170
http://jcp.org/en/jsr/detail?id=283
Más enlaces en
http://delicious.com/ajlopez/contentrepository
El primer “paper” que leí fue la introducción de Roy Fieldieng:
http://www.day.com/content/dam/day/whitepapers/JSR_170_White_Paper.pdf
El segundo (más detallado, es una especificación) es la JSR283:
http://download.oracle.com/otndocs/jcp/content_repository-2.0-pfd-oth-JSpec/
Despues de leer por arriba ambos “paperes”, empecé a pensar cómo implementar esas ideas (usando .NET; hay un proyecto de código abierto en .NET, vean SenseNet). No ví el código o la API definida en esos JSRs. Quiero llegar a una clara y simple idea de qué es lo esencial, cuáles son los conceptos núcleo a implementar. Así que el pasado fin de semana, me dediqué a un code kata: mis primeros pasos en AjCoRe, un simple repositorio de contenido:
https://github.com/ajlopez/AjCoRe
usando TDD para practicarlo (pueden ver la evolución del código en los logs de Git).
Los principales puntos son:
- Hay Workspaces identificado por nombre
- Todo Workspace tiene un Root Node
- Un Node tienen propiedades
- Una Property tiene un nombre y un valor (un valor simple, como String, DateTime, int, no es un objeto complejo)
- Un Node puede tener Child Nodes (una enumeración que puede estar vacía)
Inicialmente, en mis primeros tests, yo podía crear un Node directamente usando un constructor público. Pero en el estado actual, prefiero usar una entrada controlada para las principales operaciones, una Session. El código cliente crea una Session que maneja una Workspace.
Ahora, tengo DOS implementaciones de Workspace y Nodes (luego de un paso de refactoreo, tengo ahora un INode y un IWorkspace, ambas interfaces, y clases concretas que las implementan). Como prueba de concepto (mencionado en el “paper” de Fielding), implementé un directorio en un FileSystem, represantado por nodes de sólo lectura, con nodes FileNode y DirectoryNode. Algunas pruebas:
[TestMethod]
[DeploymentItem("Files/FileSystem", "fs")]
public void RootNodeProperties()
{
Workspace workspace = new Workspace("fs", "fs");
INode root = workspace.RootNode;
DirectoryInfo info = new DirectoryInfo("fs");
Assert.AreEqual(info.Extension, root.Properties["Extension"].Value);
Assert.AreEqual(info.FullName, root.Properties["FullName"].Value);
Assert.AreEqual(info.Name, root.Properties["Name"].Value);
Assert.AreEqual(info.CreationTime, root.Properties["CreationTime"].Value);
Assert.AreEqual(info.CreationTimeUtc, root.Properties["CreationTimeUtc"].Value);
Assert.AreEqual(info.LastAccessTime, root.Properties["LastAccessTime"].Value);
Assert.AreEqual(info.LastAccessTimeUtc, root.Properties["LastAccessTimeUtc"].Value);
Assert.AreEqual(info.LastWriteTime, root.Properties["LastWriteTime"].Value);
Assert.AreEqual(info.LastWriteTimeUtc, root.Properties["LastWriteTimeUtc"].Value);
Assert.AreEqual("fs", workspace.Name);
Assert.IsNotNull(workspace.RootNode);
Assert.AreEqual(string.Empty, workspace.RootNode.Name);
Assert.IsNull(workspace.RootNode.Parent);
}
[TestMethod]
[DeploymentItem("Files/FileSystem", "fs")]
public void GetFilesFromRoot()
{
Workspace workspace = new Workspace("fs", "fs");
INode root = workspace.RootNode;
Assert.IsNotNull(root.ChildNodes["TextFile1.txt"]);
Assert.IsNotNull(root.ChildNodes["TextFile1.txt"]);
}
[TestMethod]
[DeploymentItem("Files/FileSystem", "fs")]
public void GetFileProperties()
{
Workspace workspace = new Workspace("fs", "fs");
INode root = workspace.RootNode;
INode file = root.ChildNodes["TextFile1.txt"];
FileInfo info = new FileInfo("fs/TextFile1.txt");
Assert.AreEqual(info.Extension, file.Properties["Extension"].Value);
Assert.AreEqual(info.FullName, file.Properties["FullName"].Value);
Assert.AreEqual(info.Name, file.Properties["Name"].Value);
Assert.AreEqual(info.CreationTime, file.Properties["CreationTime"].Value);
Assert.AreEqual(info.CreationTimeUtc, file.Properties["CreationTimeUtc"].Value);
Assert.AreEqual(info.LastAccessTime, file.Properties["LastAccessTime"].Value);
Assert.AreEqual(info.LastAccessTimeUtc, file.Properties["LastAccessTimeUtc"].Value);
Assert.AreEqual(info.LastWriteTime, file.Properties["LastWriteTime"].Value);
Assert.AreEqual(info.LastWriteTimeUtc, file.Properties["LastWriteTimeUtc"].Value);
}
[TestMethod]
[DeploymentItem("Files/FileSystem", "fs")]
public void GetDirectoriesFromRoot()
{
Workspace workspace = new Workspace("fs", "fs");
INode root = workspace.RootNode;
Assert.IsNotNull(root.ChildNodes["Subfolder1"]);
Assert.IsNotNull(root.ChildNodes["Subfolder2"]);
}
Algunas notas:
- Workspace usado en el código de arriba es la clase AjCoRe.FileSystem.Workspace .
- Su constructor toma dos argumentos: el nombre del workspace en el repositorio de contenido, y el nombre del directorio (puede ser relativo) a representar.
- Los objetos INode son instancias de las clases concretas FileNode or DirectoryNode.
- Las propiedades de un archivo o directorio se ven reflejadas como valores simples en propiedades. Los valores se toman desde objetos FileInfo DirectoryInfo de System.IO)
- La propiedad DirectoryNode ChildNodes es calculada dinámicamente: cada invocación a ella calcula cuáles son los archivos y directorios del directorio representado por el nodo:
public NodeList ChildNodes
{
get
{
NodeList nodes = new NodeList();
foreach (var di in this.info.GetDirectories())
nodes.AddNode(new DirectoryNode(this, di.Name, di));
foreach (var fi in this.info.GetFiles())
nodes.AddNode(new FileNode(this, fi.Name, fi));
return nodes;
}
}
Es tiempo de presentar las dos abstracciones principales que fueron surgiendo de este trabajo inicial, INode:
public interface INode
{
string Name { get; }
INode Parent { get; }
PropertyList Properties { get; }
NodeList ChildNodes { get; }
string Path { get; }
}
y la interface IWorkspace:
public interface IWorkspace
{
string Name { get; }
INode RootNode { get; }
}
Veqn que no necesité tener un identificar único para un nodo dentro de un workspace (como piden las JSR). Cada Node tiene un Path que consiste en los nombres concatenados de la serie de nodos padres, usando / como separador). Debería implementar la recuperación de un nodo en particular usando su trayectoraria. Tampoco necesité todavía tener un NodeType por nodo, describiendo las propiedades obligatorias o sus características. Estoy siguiendo el principio YAGNI ;-)
La otra implementación concreta de IWorkspace/INode maneja nodos y propiedades en memoria. Los nodos pueden ser creados y removidos por código, y los valores de las propiedades pueen cambiar. Es mi principal implementación y que quiero extender. La pieza principal a agregar: un IStore que pueda recuperar y grabar los nodos modificados en un almacen de persistencia, con varias implementaciones: en base de datos, en bases NoSql, en archivos JSON dentro de directorios que reflejan la jerarquía de nodos, en la nube, en Azure Blob Storage, en Azure Tables, etc...
La creación de un AjCoRe.Base.Workspace:
Workspace workspace = new Workspace("ws1", null);
El segundo parémetro es la lista de propiedades para colocar en el nuevo Root Node (que tiene nombre string vacío).
Podemos obtener una sesión:
Session session = new Session(workspace);
Podemos navegar la jerarquía de nodos como en el anterior ejemplo, usando workspace.RootNode y la enumeración ChildNodes. PERO, para modificarlos, debemos usar una transacción:
INode node = session.Workspace.RootNode;
using (var tr = session.OpenTransaction())
{
session.SetPropertyValue(node, "Name", "Adam");
session.SetPropertyValue(node, "Age", 800);
tr.Complete();
}
TENEMOS que completar la transacción explícitamente con tr.Complete(). Si no lo hacemos, las propiedades cambiadas volverán a sus anteriores valores. La creación y remoción de nodos también es manejada por la transacción. Podemos crear un nuevo nodo con sus valores iniciales de propiedades:
INode root = session.Workspace.RootNode;
using (var tr = session.OpenTransaction())
{
INode node = session.CreateNode(root, "person1", new List<Property>()
{
new Property("Name", "Adam"),
new Property("Age", 800)
});
tr.Complete();
}
O podemos agregar, cambiar, remover (colocando su valor en null) propiedades dentro de una transacción:
INode root = session.Workspace.RootNode;
using (var tr = session.OpenTransaction())
{
session.SetPropertyValue(root, "Name", "Adam");
session.SetPropertyValue(root, "Age", 800);
tr.Complete();
}
Entonces, usando la sesión como punto de entrada a todas las modificaciones, puedo llevar un Unit of Work de esos cambios, sin necesidad de estar observando las propiedades y nodos que el programador recupera (si hubiera adoptado esta otra manera, debería estar vigilando a cada momento cómo un programador obtiene un nodo y lo modifica). Tengo planes de implementar algo como Software Transactional Memory para soportar concurrencia (tengo algo de código así en AjTalk y en AjSharp).
Próximos posts: detalles de implementación (transacciones, factorías de sesiones, registro de los workpsaces, etc...)
Próximos pasos: implementar persistencia en un almacén, y transacciones concurrentes.
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez
Estoy aprendiendo y practicando Ruby, y como es costumbre, lo hago escribiendo algo interesante para mí: el intérprete AjLisp (hace unos meses lo implementé en Javascript). TDD es mi amigo: escribo un test, lo ejecuto en rojo, codifico para pasarlo a verde, refactorear, y así sigue. El código de este nuevo intérprete, trabajo en progreso, en:
https://github.com/ajlopez/AjLispRb
Inicialmente (pueden ver los logs) escribí todo en un solo archivo (código y tests), siguiendo el simple y claro ejemplo:
http://kanemar.com/2006/03/04/screencast-of-test-driven-development-with-ruby-part-1-a-simple-example/
Vean que Ruby tiene un paquete ‘test/unit’ que ya viene en su instalación, listo para usar. Despues de algo de “research”, dividí el archivo en código de producción y código de pruebas. Quiero llegar a armar una gema (un paquete Ruby, distribuido por el utilitario gems), así que estudié los primeros pasos del tutorial:
http://guides.rubygems.org/
Tengo pendiente de leer y estudiar:
http://speakerdeck.com/u/pat/p/cut-polish-a-guide-to-crafting-gems
http://blog.thepete.net/2010/11/creating-and-publishing-your-first-ruby.html
Así que mi código aún no es una gema. Pero va teniendo la estructura de una:
El directorio lib contiene un solo archivo ajlisp.rb:
require 'ajlisp/list.rb'
require 'ajlisp/named_atom.rb'
require 'ajlisp/context.rb'
require 'ajlisp/string_source.rb'
require 'ajlisp/token.rb'
require 'ajlisp/lexer.rb'
require 'ajlisp/parser.rb'
require 'ajlisp/primitive.rb'
require 'ajlisp/primitive_first.rb'
require 'ajlisp/primitive_rest.rb'
require 'ajlisp/primitive_cons.rb'
require 'ajlisp/primitive_list.rb'
require 'ajlisp/primitive_closure.rb'
require 'ajlisp/fprimitive.rb'
require 'ajlisp/fprimitive_quote.rb'
require 'ajlisp/fprimitive_lambda.rb'
require 'ajlisp/fprimitive_flambda.rb'
require 'ajlisp/fprimitive_let.rb'
require 'ajlisp/fprimitive_closure.rb'
require 'ajlisp/fprimitive_define.rb'
require 'ajlisp/primitive_add.rb'
module AjLisp
@context = Context.new
@context.setValue "quote", FPrimitiveQuote.instance
@context.setValue "first", PrimitiveFirst.instance
@context.setValue "rest", PrimitiveRest.instance
@context.setValue "cons", PrimitiveCons.instance
@context.setValue "list", PrimitiveList.instance
@context.setValue "lambda", FPrimitiveLambda.instance
@context.setValue "flambda", FPrimitiveFLambda.instance
@context.setValue "let", FPrimitiveLet.instance
@context.setValue "define", FPrimitiveDefine.instance
@context.setValue "+", PrimitiveAdd.instance
def self.context
return @context
end
def self.evaluate(context, item)
if item.is_a? List or item.is_a? NamedAtom
return item.evaluate(context)
end
return item
end
end
Escribí algunas primitivas (formas normales, y formas especiales: estas últimas no evalúan sus parámetros antes de su aplicación, ejemplos: quote y define). Noten que los archivos adicionales los puse en un subdirectorio ajlisp dentro de lib, ¿por qué? Porque cuando este código sea instalado como una gema, todo el directorio lib estará disponible para require, y si hubiera un archivo ahí, se podría hacer require(‘elarchivo’). Es por eso que los archivos adicionales a ajlisp.rb se colocan en otro lado, evitando colisión de nombres. Se recomienda colocarlos debajo de lib (vean el código de gemas que tienen en su instalación de Ruby, o vean ejemplos en GitHub).
El el directorio test hay un archivo test.rb que incluye a los otros archivos de tests:
require 'ajlisp'
require 'test/unit'
require "test_list.rb"
require "test_named_atom.rb"
require "test_context.rb"
require "test_string_source.rb"
require "test_token.rb"
require "test_lexer.rb"
require "test_parser.rb"
require "test_primitive_first.rb"
require "test_primitive_rest.rb"
require "test_primitive_cons.rb"
require "test_primitive_list.rb"
require "test_primitive_closure.rb"
require "test_primitive_add.rb"
require "test_fprimitive_quote.rb"
require "test_fprimitive_lambda.rb"
require "test_fprimitive_let.rb"
require "test_fprimitive_closure.rb"
require "test_fprimitive_flambda.rb"
require "test_fprimitive_define.rb"
require "test_evaluate"
Pueden ejecutar los tests desde la línea de comando:
ruby –Ilib;test test\test.rb
En Windows, dejé el archivo runtest.cmd conteniendo esta línea. Los parámetros –Ilib;test le indican a Ruby que incluya los directorios lib y test para cuando tenga que resolver un require. De esta forma evito poner directorios explícitos (o usar __FILE__) en los require.
Algo de tests:
require 'ajlisp'
require 'test/unit'
class TestList < Test::Unit::TestCase
#...
def test_create_with_first
list = AjLisp::List.new("foo")
assert_equal("foo", list.first)
assert_nil(list.rest)
end
def test_create_with_first_and_rest
rest = AjLisp::List.new("bar")
list = AjLisp::List.new("foo", rest)
assert_equal("foo", list.first)
assert_not_nil(list.rest)
assert_equal("bar", list.rest.first)
assert_nil(list.rest.rest)
end
def test_create_from_array
list = AjLisp::List.make [1, "a", "foo"]
assert_not_nil list
assert_equal 1, list.first
assert_equal "a", list.rest.first
assert_equal "foo", list.rest.rest.first
assert_nil list.rest.rest.rest
end
#..
end
Cada lista en AjLisp es un objeto de esta clase, list.rb:
module AjLisp
class List
attr_reader :first
attr_reader :rest
def initialize(first=nil, rest=nil)
@first = first
@rest = rest
end
def evaluate(context)
form = AjLisp::evaluate(context, @first)
form.evaluate(context, self)
end
def self.make(array)
if array and array.length > 0
first = array.shift
if first.is_a? Array
first = make(first)
elsif first.is_a? Symbol
first = NamedAtom.new first.to_s
end
return List.new first, make(array)
end
return nil
end
end
end
Los méteodos de acceso first y rest son de sólo lectura. Gracias a la naturaleza no tipada de Ruby (facilidad que también encontré en la implementación de Javascript) la implementación de este intérprete es directa, sin mayor “ceremonia de código”.
En mis nuevos tests, ahora incluye el código DENTRO del módulo AjLisp, así me evito de escribir el prefijo AjLisp:: antes de referenciar a una clase:
require 'ajlisp'
require 'test/unit'
module AjLisp
class TestLexer < Test::Unit::TestCase
def test_get_atom_token
source = StringSource.new "atom"
lexer = Lexer.new source
token = lexer.nextToken
assert_not_nil token
assert_equal "atom", token.value
assert_equal TokenType::ATOM, token.type
assert_nil lexer.nextToken
end
def test_get_atom_token_with_spaces
source = StringSource.new " atom "
lexer = Lexer.new source
token = lexer.nextToken
assert_not_nil token
assert_equal "atom", token.value
assert_equal TokenType::ATOM, token.type
assert_nil lexer.nextToken
end
#...
end
Próximos tópicos: algunos detalles de implementación, primitives vs fprimitives, contexto (ambiente anidado con pares nombre/valor), lambdas y closures, el lexer y el parser.
Próximos pasos: completar las primitivas (let, letrec, definef, do, if…), macro (mlambda, definem, expansión de macros…)
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez
Anterior Post
Siguiente Post
Dos semanas atrás, fue publicada una nueva versión del Windows Azure Toolkit for Social Games. Vean los posts de @ntotten:
Windows Azure Toolkit for Social Games Version 1.1.1
Windows Azure Toolkit for Social Games Version 1.1
La nueva versión implementa dos juegos simples en HTML5: Ta Te Ti, y Cuatro en Raya, usando vistas ASP.NET MVC, servicios WCF Web API, seguridad federada usando ACS, y Azure Storage. Pueden jugar en línea en:
http://watgames4.cloudapp.net/
Pienso que éste es un ejemplo más claro que el anterior (Tankster) que era una gran aplicación pero algo excesiva ;-). Veamos de entrar en algunos detalles de implementación de este nuevo ejemplo.
Totten escribió:
The biggest change we have made in this release is to separate the core toolkit from the Tankster game. After we released the Tankster sample game we received a lot of feedback asking for a simpler game that developers could use to learn. To meet this need we developed two simple games, Tic-Tac-Toe and Four in a Row, and included in the toolkit. The Tankster game is now available separately as a sample built on top of the toolkit.
…
While the new games included in the toolkit are much simpler than Tankster, they still show the same core concepts. You can easily use these samples as a starting point to build out any number of types of games. Additionally, you will find that many of the core components of the game such as the leaderboard services, game command services can be used without any modification to the server side or client side code.
En mi anterior post, mencioné un pequeño pero importante cambio en el proceso de acciones de juego: toda la lógica fue removida del código del servidor. Adoptando este camino, podemos escribir nuevos juegos sin cambiar el código del servidor. Podemos seguir agregando código en el servidor si lo necesitamos (por ejemplo, para agregar control al juego, detectar operaciones inválidas enviadas desde algún cliente, etc) pero es interesante tener una base de código que sea agnóstica del juego.
Abriendo la solución en Visual Studio, encontraremos archivos Javascript usados por los dos juegos. Podemos escribir un nuevo juego, reusando estos archivos sin cambios:
Los juegos están implementados como áreas:
Podríamos escribir nuevos juegos y publicarlos como paquetes NuGet.
Visualmente, el código Javascript cliente está organizado de esta manera:
Cada juego X (X = Ta Te Ti, Cuatro en Raya, uno nuestro) tiene:
- XGame: la lógica del juego
- XBoard: para dibujar el tablero en un elemento canvas de HTML5, y para detectar eventos de click en el mismo.
- XViewModel: contiene el jugador actual y otros datos para ser usandos en el armado de la vista (los ejemplos usan knockout.js, como MVC en Javascript)
- XController: para procesar nuevos eventos y para coordinar los elementos de arriba.
La parte genérica:
- UserService: métodos relacionados a usuarios: login, lista de amigos, etc…
- GameService: jugar las movidas, recibir movidas de otros jugadores, otras acciones (por ejemplo, se podrían enviar mensajes de chat).
- ServerInterface: Llamadas Ajax (usando GET, POST, JSONP, Azure storage….) que son usados por la implementación de User y Game Service.
Temas para próximos posts: analizar el código Javascript, el uso del Canvas, tests de Javascript usando QUnit, comunicación con el servidor usando Ajax, cambio del Game Service (en Javascript) para usar un Node.js como servidor que reciba y reparta las acciones de juego.
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez