changeset 248:44099896397c

merege work
author hychen@hychen-desktop
date Mon, 01 Dec 2008 15:32:44 +0800
parents 7747bbe5b68e (diff) a344c54f15c7 (current diff)
children b5897d63f44e
files
diffstat 113 files changed, 5240 insertions(+), 904 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Thu Oct 23 00:47:56 2008 +0800
+++ b/.hgignore	Mon Dec 01 15:32:44 2008 +0800
@@ -1,10 +1,14 @@
 syntax: glob
 .eagleeye.pm
 .hgignore
+dump.yaml
 ikariam.sqlite
-perl
-samples
+warfare-dump.yaml
 *.html
 *.sh
 *.txt
+*-dump.yaml
+excel
+perl
+samples
 ecmanaut-read-only
--- a/Ikariam.pm	Thu Oct 23 00:47:56 2008 +0800
+++ b/Ikariam.pm	Mon Dec 01 15:32:44 2008 +0800
@@ -1,4 +1,3 @@
-#!/usr/bin/env perl
 BEGIN {
     foreach (((getpwuid($<))[7], $ENV{HOME}, $ENV{LOGDIR}, ".")) {
         require "$_/.eagleeye.pm" if (-f "$_/.eagleeye.pm");
@@ -8,21 +7,100 @@
 use Class::DBI::AutoLoader (
     dsn       => 'dbi:SQLite:dbname=ikariam.sqlite',
     options   => { RaiseError => 1 },
-    tables    => ['cities', 'island', 'user'],
+    tables    => ['cities', 'island', 'user', 'ally', 'report'],
     use_base  => 'Class::DBI::SQLite',
     namespace => 'Ikariam',
 );
 
+package Ikariam::Extractor;
+use strict;
+use Data::Dumper;
+use XML::LibXML;
+use IO::Uncompress::Gunzip qw(gunzip $GunzipError) ;
+use Carp;
+use utf8;
+
+sub new {
+    Carp::croak("Options should be key/value pairs, not hash reference") if ref($_[1]) eq 'HASH'; 
+
+    my($class, %conf) = @_;
+
+    my $self = bless {
+        doc => undef,
+        gzip => 1,
+    }, $class;
+
+    $self->{gzip} = $conf{'gzip'} if(defined($conf{'gzip'}));
+    $self->parse($conf{'content'}) if(defined($conf{'content'}));
+
+    return $self;
+}
+
+sub parse {
+    my ($self, $content) = @_;
+    my $string;
+    my $parser = XML::LibXML->new('1.0','UTF-8');
+
+    if($self->{gzip} == 1) {
+        gunzip \$content => \$string
+            or die "gunzip failed: $GunzipError\n";
+    } else {
+        $string = $content;
+    }
+
+    # dirty hack for stupid highScore search function.
+    $string =~ s/name="searchUser" value=".*?"/name="searchUser" value=""/;
+
+    $self->{doc} = $parser->parse_html_string ($string, { suppress_errors => 1, encoding => 'UTF-8' });
+    return;
+}
+
+# find($XPathQuery);
+sub find {
+    my $self = shift;
+    my $query = shift;
+    my $out = [];
+
+    my $result = $self->{doc}->find($query);
+
+    return undef unless defined($result);
+
+    if ( $result->isa( 'XML::LibXML::NodeList' ) ) {
+        if($result->size() == 0) {
+            return undef unless wantarray;
+            return ();
+        }
+        foreach ( @$result ) {
+            # $_ is XML::LibXML::Element, XML::LibXML::Node
+            my $literal = $_->to_literal();
+            utf8::encode($literal);
+            # warn $_->toString(1) , "\n";
+            return $literal unless wantarray;
+            push( @$out, $literal);
+        }
+    } else {
+        Carp::croak("Unsupported data type"); 
+    }
+    # TODO
+    # XML::LibXML::Literal
+    # XML::LibXML::Boolean
+
+    return @$out;
+}
+
+1;
+
 package Ikariam;
 use strict;
 use Data::Dumper;
 use LWP;
-# use LWP::Debug qw(+ -conns -trace -debug);
-# use LWP::Debug qw(+trace);
+
 use HTTP::Cookies;
 use WWW::Mechanize;
-use HTML::TagParser;
+use XML::LibXML qw(:encoding);
 use IO::Uncompress::Gunzip qw(gunzip $GunzipError) ;
+use POSIX;
+use utf8;
 
 sub new
 {
@@ -37,95 +115,85 @@
         server => $server,
         user => $user,
         pass => $pass,
-        buildingIDs => {
-            townHall =>  0,
-            townhall =>  0,
-            port =>  3,
-            academy =>  4,
-            shipyard =>  5,
-            barracks =>  6,
-            warehouse =>  7,
-            wall =>  8,
-            tavern =>  9,
-            museum =>  10,
-            palace =>  11,
-            embassy =>  12,
-            branchOffice =>  13,
-            workshop =>  15,
-            'workshop-army' =>  15,
-            'workshop-fleet' =>  15,
-            safehouse =>  16,
-            palaceColony =>  17,
-            resource =>  1,
-            tradegood =>  2
-        }
+        debug => undef,
     };
 
 
+    # if debug
+    LWP::Debug::level('+trace');
+
     $self->{mech}->cookie_jar(HTTP::Cookies->new(file => "/tmp/ikariam-cookies.txt", autosave => 1));
     $self->{mech}->default_headers->push_header('Accept-Encoding', 'deflate');
  
     return bless $self, $class;
 }
 
+sub viewAlly
+{
+    my $self = shift;
+    my $allyId = shift;
+
+    my $res = $self->{mech}->get(sprintf("http://%s/index.php?view=allyPage&allyId=%d", $self->{server}, $allyId));
+    my $extractor = new Ikariam::Extractor(content => $res->content);
+
+    my %ally;
+    $ally{'id'} = $allyId;
+    $ally{'name'} = $extractor->find("//table[\@id='allyinfo']/tr[1]/td[2]/text()");
+    $ally{'members'} = $extractor->find("//table[\@id='allyinfo']/tr[2]/td[2]/text()");
+    $ally{'score'} = $extractor->find("//table[\@id='allyinfo']/tr[4]/td[2]/text()");
+    $ally{'score'} =~ s/\d+ \(([\d,]+)\)/$1/;
+    $ally{'score'} =~ s/,//g;
+
+    $ally{'url'} = $extractor->find("//table[\@id='allyinfo']/tr[5]/td[2]/text()");
+    delete($ally{'url'}) if($ally{'url'} eq '-');
+
+    return \%ally;
+}
+
 sub viewScore
 {
     my $self = shift;
     my $type = shift || 'score';
     my $user = shift || '';
     my $offset = shift || 0;
-
-    my $res = $self->{mech}->post(sprintf("http://%s/index.php", $self->{server}), [
-        highscoreType => $type,
-        offset => $offset,
-        searchUser => $user,
-        view => 'highscore'
-        ]);
+    my %users;
 
-    my $c;
-    my $status = gunzip \$res->content => \$c 
-        or die "gunzip failed: $GunzipError\n";
+    $self->{mech}->get(sprintf("http://%s/index.php?view=highscore&showMe=1", $self->{server}));
+    my $res = $self->{mech}->submit_form(
+        form_number => 1,
+        fields      => {
+            highscoreType => $type,
+            offset => $offset,
+            searchUser => $user,
+        }
+   );
 
-    my %users;
-    my $html = HTML::TagParser->new($c);
-    my ($table) = $html->getElementsByAttribute("class", "table01");
-    return %users if(!defined($table));
-
-    my @elems = getElementsByTagName($table, "tr");
+    my $extractor = new Ikariam::Extractor(content => $res->content);
+    my $result = $extractor->{doc}->find('//table[@class="table01"][2]//tr');
 
-    foreach my $elem (@elems) {
-        my $e;
+    foreach my $tr ( @$result ) {
         my %user;
+        my $extractor = new Ikariam::Extractor(content => $tr->toString(0));
 
-        $e = getElementsByAttribute($elem, "class", "action");
-        $e = getElementsByTagName($e, "a");
-
-        if(defined ($e) && $e->getAttribute('href') =~ /index\.php\?view=sendMessage&with=(\d+)&oldView=highscore/)
-        {
+        my $href = $extractor->find('//td[@class="action"]/a/@href');
+        if($href =~ /index\.php\?view=sendMessage&with=(\d+)&oldView=highscore/) {
             $user{'id'} = $1;
-
-            $e = getElementsByAttribute($elem, "class", "name");
-            $user{'name'} = $e->innerText();
+            # $user{'name'} = $user;
+            # encoding issue.
+            $user{'name'} = $extractor->find('//td[@class="name"]/text()');
+            next if($user{'name'} eq '');
 
-            $e = getElementsByAttribute($elem, "class", "allytag");
-            $user{'ally'} = $e->innerText();
-
-            $e = getElementsByTagName($e, "a");
-            if($e->getAttribute('href') =~ /\?view=allyPage&allyId=(\d+)/)
-            {
+            $user{'ally'} = $extractor->find('//td[@class="allytag"]/a/text()');
+            my $allyHref = $extractor->find('//td[@class="allytag"]/a/@href');
+            if($allyHref =~ /\?view=allyPage&oldView=highscore&allyId=(\d+)/) {
                 $user{'allyId'} = $1;
             }
-
-            $e = getElementsByAttribute($elem, "class", "score");
-            $user{$type} = $e->innerText();
+            $user{$type} = $extractor->find('//td[@class="score"]/text()');
             $user{$type} =~ s/,//g;
-
             $users{$user{'id'}} = \%user;
-        } else {
-            next;
         }
     }
-
+    # print(Dumper(\%users));
     return \%users;
 }
 
@@ -135,8 +203,7 @@
     my $x = shift;
     my $y = shift;
 
-    if(!defined($x) && !defined($y))
-    {
+    if(!defined($x) && !defined($y)) {
         die('location required');
     }
 
@@ -211,54 +278,52 @@
     my $island = shift;
 
     my $res = $self->{mech}->get(sprintf("http://%s/index.php?view=island&id=%s", $self->{server}, $island));
-
-    my $c;
-    my $status = gunzip \$res->content => \$c 
-        or die "gunzip failed: $GunzipError\n";
-
-    my $html = HTML::TagParser->new($c);
+    my $extractor = new Ikariam::Extractor(content => $res->content);
 
-    # find inactivity and vacation
-    my %status;
-    foreach my $class (qw/inactivity vacation/)
-    {
-        my @elems = $html->getElementsByAttribute("class", $class);
-        foreach my $elem (@elems) {
-            if($elem->innerText() =~ /^(.*?) \((\w)\)/) {
-                $status{$1} = $2;
-                # printf("%s\n", $elem->innerText());
+    my @cities;
+    foreach my $i (0..16) {
+        my $cityLocations = $extractor->find(sprintf('//li[@id="cityLocation%s"]/@class', $i));
+        if($cityLocations =~ /city level(\d+)/) {
+            my %info;
+            $info{'citylevel'} = $1;
+            $info{'cityname'} = $extractor->find(sprintf('//li[@id="cityLocation%s"]//li[@class="name"]/text()', $i));
+            $info{'owner'} = $extractor->find(sprintf('//li[@id="cityLocation%s"]//li[@class="owner"]/text()', $i));
+            $info{'owner'} =~ s/\s+//g;
+            $info{'ally'} = $extractor->find(sprintf('//li[@id="cityLocation%s"]//li[@class="ally"]/a/text()', $i));
+            if($info{'ally'} eq '-') {
+                delete($info{'ally'}) 
+            } else {
+                my $href = $extractor->find(sprintf('//li[@id="cityLocation%s"]//li[@class="ally"]/a/@href', $i));
+                if($href =~ /&allyId=(\d+)&/) {
+                    $info{'allyId'} = $1;
+                }
             }
-        }
-    }
-
-    # find content
-    my @elems = $html->getElementsByClassName( "cityinfo" );
-    my @cities;
-    foreach my $elem (@elems) {
-        my %info;
 
-        my @e = getElementsByTagName($elem, "li");
-        $info{'cityname'} = substr($e[0]->innerText(), 8);
-        $info{'citylevel'} = substr($e[1]->innerText(), 14);
-        $info{'owner'} = substr($e[2]->innerText(), 8);
-        $info{'ally'} = substr($e[3]->innerText(), 8);
-        delete($info{'ally'}) if($info{'ally'} eq '-');
+            # Ally Id
+            my $href = $extractor->find(sprintf('//li[@id="cityLocation%s"]//a[@class="messageSend"]/@href', $i));
+            if ($href =~ /with=(\d+)&destinationCityId=(\d+)/) {
+                $info{'user'} = $1;
+                $info{'cityId'} = $2;
+            } else {
+                # 聯盟 this is me.
+                my $id = $extractor->find(sprintf('//li[@id="cityLocation%s"]/a/@id', $i));
+                if($id =~ /city_(\d+)/) {
+                    $info{'user'} = undef; # FIXME
+                    $info{'cityId'} = $1; 
+                }
+            }
 
-        @e = getElementsByAttribute($elem, "class", "messageSend");
-        if ( $e[0]->getAttribute("href") =~ /with=(\d+)&destinationCityId=(\d+)/)
-        {
-            $info{'user'} = $1;
-            $info{'cityId'} = $2;
+            if(defined(($extractor->find(sprintf('//li[@id="cityLocation%s"]//span[@class="vacation"]', $i)))[0])) {
+                $info{'status'} = 'v';
+            }
+            if(defined(($extractor->find(sprintf('//li[@id="cityLocation%s"]//span[@class="inactivity"]', $i)))[0])) {
+                $info{'status'} = 'i';
+            }
+            push @cities, \%info;
+        } else {
+            # TODO: delete the old city.
         }
 
-        # update status;
-        if(defined($status{$info{'cityname'}})) {
-            $info{'status'} = $status{$info{'cityname'}};
-        } else {
-            $info{'status'} = undef;
-        }
-        # print(Dumper(\%info));
-        push @cities, \%info;
     }
 
     return @cities;
@@ -269,6 +334,12 @@
     my $param = shift;
     my $cityId = shift;
 
+    # TODO: 應該扣除買船之黃金, 以免誤計
+    # TODO: 加上所有之船隻
+    # my @cargoCost = qw/160 244 396 812 1240 1272 1832 1888 3848 3972 5204 5384 6868 7120 8864 9200 11268 11712 14108 14680 23320 24288 28664 29880 34956 36468 42348 44212 51024 53308 61236 64024 73096 76468 87020 91088 103224 116524 122072 137432 180060 202132 211964 237444 249108 278276 292076 306623 321963 338138 355198 373191 392171 412195 433320 455612 479135 503962 530166 557828 587031 617863 650420 684802 721113 759466 799981 842783 888005 935790 986286 1039654 1096062 1155689 1218724 1285369 1355837 1430353 1509159 1592508 1680670 1773932 1872597 1976989 2087448 2204338 2328045 2458976 2597567 2744276 2899594 3064040 3238163 3422550 3617820 3824635 4043693 4275738 4521561 4782000 5057946 5350345 5660202 5988585 6336630 6705540 7096598 7511164 7950683 8416694 8910828 9434823 9990523 10579889 11205006 11868090 12571498 13317734 14109462 14949514/;
+    # $city->{transporters}->{sum}
+ 
+    # TODO: 找買船最便宜之城市
     my @locations = @{$self->{'cities'}->{$cityId}->{locations}};
     foreach (1..2) {
         if($locations[$_] eq 'port') {
@@ -278,6 +349,204 @@
     }
 }
 
+
+# for tavern only
+sub set {
+    my $self = shift;
+    my $type = shift;
+    my $cityId = shift;
+
+    die ("we don't know about this city") unless(defined($self->{'cities'}->{$cityId}));
+
+    my $position = -1;
+    my @locations = @{$self->{'cities'}->{$cityId}->{locations}};
+    foreach (0..$#locations) {
+        $position = $_ if($locations[$_] eq $type);
+    }
+
+# Academy - inputScientists
+    if($position != -1 && $type eq 'academy') {
+        $self->{mech}->get (sprintf("http://%s/index.php?view=%s&id=%s&position=%d", $self->{server}, $type, $cityId, $position));
+        $self->{mech}->submit_form(
+                form_number => 1,
+                fields      => {
+                    s => $self->{'cities'}->{$cityId}->{$type}->{maxValue},
+                }
+                );
+
+    }
+
+# Tavern
+    if($position != -1 && $type eq 'tavern') {
+        $self->{mech}->get (sprintf("http://%s/index.php?view=%s&id=%s&position=%d", $self->{server}, $type, $cityId, $position));
+        $self->{mech}->submit_form(
+                form_number => 1,
+                fields      => {
+                    amount => $self->{'cities'}->{$cityId}->{$type}->{maxValue},
+                }
+                );
+
+    }
+}
+
+sub is_shipyard_upgrading {
+    my $self = shift;
+    my $cityId = shift;
+    my $type = "shipyard";
+
+    my $position = -1;
+    my @locations = @{$self->{'cities'}->{$cityId}->{locations}};
+    foreach (1..2) {
+        $position = $_ if($locations[$_] eq $type);
+    }
+
+    if($position != -1) {
+        my $res = $self->{mech}->get (sprintf("http://%s/index.php?view=%s&id=%s&position=%d", $self->{server}, $type, $cityId, $position));
+        if(defined(Ikariam::Extractor->new(content => $res->content)->find('//div[@class="isUpgrading"]'))) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+    return 0;
+}
+
+sub is_navy_trainning {
+    my $self = shift;
+    my $cityId = shift;
+    my $type = "shipyard";
+
+    my $position = -1;
+    my @locations = @{$self->{'cities'}->{$cityId}->{locations}};
+    foreach (1..2) {
+        $position = $_ if($locations[$_] eq $type);
+    }
+
+    if($position != -1) {
+        my $res = $self->{mech}->get (sprintf("http://%s/index.php?view=%s&id=%s&position=%d", $self->{server}, $type, $cityId, $position));
+        if(defined(Ikariam::Extractor->new(content => $res->content)->find('//div[@id="unitConstructionList"]//h4'))) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+    # FIXME we can not found the shipyard
+    return 0;
+}
+
+sub buildShips {
+    my $self = shift;
+    my $shipType = shift;
+    my $cityId = shift;
+    my $type = 'shipyard';
+
+    warn("buildShips $shipType");
+    my $position = -1;
+    my @locations = @{$self->{'cities'}->{$cityId}->{locations}};
+    foreach (1..2) {
+        $position = $_ if($locations[$_] eq $type);
+    }
+
+    if($position != -1) {
+        my $res = $self->{mech}->get (sprintf("http://%s/index.php?view=%s&id=%s&position=%d", $self->{server}, $type, $cityId, $position));
+        $self->{mech}->submit_form(
+                form_number => 1,
+                fields      => {
+                    $shipType => 1,
+                }
+                );
+    }
+}
+
+sub is_army_trainning {
+    my $self = shift;
+    my $cityId = shift;
+    my $type = "barracks";
+
+    my $position = -1;
+    my @locations = @{$self->{'cities'}->{$cityId}->{locations}};
+    foreach (2..$#locations) {
+        $position = $_ if($locations[$_] eq $type);
+    }
+
+    if($position != -1) {
+        my $res = $self->{mech}->get (sprintf("http://%s/index.php?view=%s&id=%s&position=%d", $self->{server}, $type, $cityId, $position));
+        if(defined(Ikariam::Extractor->new(content => $res->content)->find('//div[@id="unitConstructionList"]//h4'))) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+    # FIXME we can not found the shipyard
+    return 0;
+}
+
+sub is_barracks_upgrading {
+    my $self = shift;
+    my $cityId = shift;
+    my $type = 'barracks';
+
+    my $position = -1;
+    my @locations = @{$self->{'cities'}->{$cityId}->{locations}};
+    foreach (2..$#locations) {
+        $position = $_ if($locations[$_] eq $type);
+    }
+
+    if($position != -1) {
+        my $res = $self->{mech}->get (sprintf("http://%s/index.php?view=%s&id=%s&position=%d", $self->{server}, $type, $cityId, $position));
+        if(defined(Ikariam::Extractor->new(content => $res->content)->find('//div[@class="isUpgrading"]'))) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+    return 0;
+}
+
+sub buildUnits {
+    my $self = shift;
+    my $unitType = shift;
+    my $cityId = shift;
+    my $type = 'barracks';
+
+    warn("buildShips $unitType");
+    my $position = -1;
+    my @locations = @{$self->{'cities'}->{$cityId}->{locations}};
+    foreach (2..$#locations) {
+        $position = $_ if($locations[$_] eq $type);
+    }
+
+    if($position != -1) {
+        my $res = $self->{mech}->get (sprintf("http://%s/index.php?view=%s&id=%s&position=%d", $self->{server}, $type, $cityId, $position));
+        $self->{mech}->submit_form(
+                form_number => 1,
+                fields      => {
+                    $unitType => 1,
+                }
+                );
+    }
+}
+
+sub buildSpy {
+    my $self = shift;
+    my $unitType = shift;
+    my $cityId = shift;
+    my $type = 'safehouse';
+
+    my $position = -1;
+    my @locations = @{$self->{'cities'}->{$cityId}->{locations}};
+    foreach (2..$#locations) {
+        $position = $_ if($locations[$_] eq $type);
+    }
+
+    if($position != -1) {
+        my $res = $self->{mech}->get (sprintf("http://%s/index.php?view=%s&id=%s&position=%d", $self->{server}, $type, $cityId, $position));
+        my $url = 
+            Ikariam::Extractor->new(content => $res->content)->find(sprintf('//div[@class="forminput"]/a/@href'));
+        $self->{mech}->get($url) if(defined($url));
+    }
+}
+
 sub build {
     my $self = shift;
     my $type = shift;
@@ -291,39 +560,40 @@
         $position = $_ if($locations[$_] eq $type);
     }
 
-    if($position == -1)
-    {
-        foreach (0..$#locations) {
-            next if($_ <= 2 && ($self->{buildingIDs}->{$type} ne "workshop-fleet" &&
-                    $self->{buildingIDs}->{$type} ne "shipyard"));
-            if($locations[$_] eq undef) {
-                my $res = $self->{mech}->get(sprintf('http://%s/index.php?action=CityScreen&function=build&id=%s&position=%s&building=%d', 
-                        $self->{server}, $cityId, $_, $self->{buildingIDs}->{$type} ));
-                last;
+    if($position == -1) {
+        my $targetPosition = undef;
+        if($type eq "wall") {
+            # 14 is wall.
+            $targetPosition = 14;
+        } else {
+            foreach (0..13) {
+                next if($_ <= 2 && ($type ne "workshop-fleet" &&
+                            $type ne "shipyard"));
+
+                if($locations[$_] eq undef) {
+                    $targetPosition = $_;
+                    last;
+                }
             }
         }
+        
+        my $res = $self->{mech}->get(sprintf('http://%s/index.php?view=buildingGround&id=%s&position=%s', 
+                        $self->{server}, $cityId, $targetPosition));
+        my $url = 
+            Ikariam::Extractor->new(content => $res->content)->find(sprintf('//li[@class="building %s"]//a/@href', $type));
+        $self->{mech}->get($url) if(defined($url));
     } else {
-        $self->{mech}->add_header( Referer =>  
-            sprintf("http://%s/index.php?view=%s&id=%s&position=%d", $self->{server}, $type, $cityId, $position));
-        my $res = $self->{mech}->post(sprintf('http://%s/index.php', $self->{server}), [
-            action => 'CityScreen',
-            'function' => 'upgradeBuilding',
-            id => $cityId,
-            position => $position,
-            level => $self->{'cities'}->{$cityId}->{buildings}->{$type},
-            oldView => $type,
-            ]);
-#        my $content;
-#        gunzip \$res->content => \$content 
-#            or die "gunzip failed: $GunzipError\n";
-#        print ($content);
+        my $res = $self->{mech}->get (sprintf("http://%s/index.php?view=%s&id=%s&position=%d", $self->{server}, $type, $cityId, $position));
+        my $url = Ikariam::Extractor->new(content => $res->content)->find('//a[@title="升級建築物"]/@href');
+        $self->{mech}->get($url) if(defined($url));
     }
 }
 
 sub run {
     my $self = shift;
+    my $param = shift;
     # defense.
-    die("Not implemented");
+    warn ("run $param not implemented yet.");
 }
 
 sub research
@@ -334,12 +604,6 @@
 
     # check if we are researching the same stuff
     my $res =  $self->{mech}->get(sprintf('http://%s/index.php?action=CityScreen&function=changeResearch&id=%s&researchType=%s', $self->{server}, $cityId, $type));
-
-#    my $content;
-#    gunzip \$res->content => \$content 
-#        or die "gunzip failed: $GunzipError\n";
-#
-#    print ($content);
 }
 
 sub checkResearch {
@@ -347,186 +611,570 @@
     my $cityId = shift;
 
     my $res = $self->{mech}->get(sprintf('http://%s/index.php?view=researchOverview&id=%s', $self->{server}, $cityId));
+    
+    my @urls = Ikariam::Extractor->new(content => $res->content)->find('//ul[@class="explored"]//a/@href');
+    my $out = {};
+    foreach(@urls) {
+        if(/view=researchDetail&id=\d+&position=\d+&researchId=(\d+)$/) {
+            # we ignore the chinese name of technology researched.
+            @$out{$1} = 1;
+        }
+    }
+    return $out;
+}
 
-    my $content;
-    gunzip \$res->content => \$content 
-        or die "gunzip failed: $GunzipError\n";
-    my $html = HTML::TagParser->new($content);
+sub plunderCity {
+    my $self = shift;
+    my $cityId = shift;
+    my $res = $self->{mech}->get(sprintf("http://%s/index.php?view=plunder&destinationCityId=%d", $self->{server}, $cityId));
+
+    my $extractor = Ikariam::Extractor->new(content => $res->content);
+    # check peace treaty
+    my $treaty = $extractor->find('//div[@class="warning"]');
+    if(!defined($treaty)) {
+        my @forms = $self->{mech}->forms();
+        if($#forms < 1) {
+            my $city = Ikariam::Cities->retrieve($cityId);
+            $city->delete;
+        } else {
+            $self->{mech}->submit_form(
+                    form_number => 1,
+                    fields      => {
+                           cargo_army_302 => '3', # 劍士
+                    }
+                    );
+        }
+    } else {
+        # put the id in the friends.txt file.
+        Ikariam::Cities->has_a(user => 'Ikariam::User');
+        my $city = Ikariam::Cities->retrieve($cityId);
+        my $sheep = $city->user;
+
+        open(OUT, ">>friends.txt") or Carp::carp("can not open friends.txt");
+        print OUT $sheep->name . "\n";
+        close(OUT);
+        Carp::carp($treaty);
+    }
+}
+
+sub changeCity {
+    my $self = shift;
+    my $cityId = shift;
+
+    $self->{mech}->get(sprintf("http://%s/index.php", $self->{server}));
+    $self->{mech}->submit_form(
+        form_number => 2,
+        fields      => {
+            cityId => $cityId,
+        }
+    );
+}
+
+sub checkCity {
+    my $self = shift;
+    my $cityId = shift;
+
+    $self->changeCity($cityId);
+
+    my $res = $self->{mech}->get(sprintf("http://%s/index.php?view=city&id=%d", $self->{server}, $cityId));
+    my $extractor = new Ikariam::Extractor(content => $res->content);
+
+    $self->{'cities'}->{$cityId}->{resources}->{gold} = $extractor->find('//span[@id="value_gold"]/text()');
+    $self->{'cities'}->{$cityId}->{resources}->{gold} =~ s/,//g;
+
+    $self->{'cities'}->{$cityId}->{name} = $extractor->find('//span[@class="city"]/text()');
+
+    my $island = $extractor->find('//div[@id="breadcrumbs"]/a[@class="island"]');
+    if($island =~ /(\w+)\[(\d+):(\d+)\]/) {
+        $self->{'cities'}->{$cityId}->{island}->{name} = $1;
+        $self->{'cities'}->{$cityId}->{island}->{x} = $2;
+        $self->{'cities'}->{$cityId}->{island}->{y} = $3;
+        $self->{'cities'}->{$cityId}->{island}->{id} = my $island = $extractor->find('//div[@id="breadcrumbs"]/a[@class="island"]/@href');
+        $self->{'cities'}->{$cityId}->{island}->{id} =~ s/\?view=island&id=(\d+)/$1/;
+    }
+    
+    $self->{'cities'}->{$cityId}->{construction} = defined($extractor->find('//*[@class="constructionSite"]')) ? 1 : 0;
+
+    # maxCapacity
+    my $page = $extractor->{doc}->toString(1);
+    if($page =~ /maxCapacity : {\s+wood: (\d+),\s+wine: (\d+),\s+marble: (\d+),\s+crystal: (\d+),\s+sulfur: (\d+)\s+}/s) {
+        $self->{'cities'}->{$cityId}->{maxCapacity}->{wood} = $1;
+        $self->{'cities'}->{$cityId}->{maxCapacity}->{wine} = $2;
+        $self->{'cities'}->{$cityId}->{maxCapacity}->{marble} = $3;
+        $self->{'cities'}->{$cityId}->{maxCapacity}->{crystal} = $4;
+        $self->{'cities'}->{$cityId}->{maxCapacity}->{sulfur} = $5;
+    }
 
-    my @elems = $html->getElementsByAttribute('class', 'explored');
+    foreach my $good (qw/wood wine marble crystal sulfur/) {
+        $self->{'cities'}->{$cityId}->{resources}->{$good} = $extractor->find(sprintf('//span[@id="value_%s"]', $good));
+        $self->{'cities'}->{$cityId}->{resources}->{$good} =~ s/,//g;
+    }
+
+    foreach my $i (0..14) {
+        my @buildings = $extractor->find(sprintf('//li[@id="position%s"]/@class', $i));
+        foreach my $building (@buildings) {
+            if (!($building =~ /buildingGround/) && !($building =~ /townhall/)) {
+                $self->{'cities'}->{$cityId}->{locations}[$i] = $building;
+
+                my $span;
+                my @spans = $extractor->find(sprintf('//li[@id="position%s"]//span[@class="textLabel"]/text()', $i));
+                if($#spans >= 1) {
+                    $span = $spans[1];
+                } else {
+                    $span = $spans[0];
+                }
+                my (undef, undef, $level) = split(/ /, $span);
+                $self->{'cities'}->{$cityId}->{buildings}->{$building} = $level;
+            }
+        }
+    }
+
+    $self->{'cities'}->{$cityId}->{transporters}->{avail} = $extractor->find('//span[@id="value_transAvail"]/text()');
+    $self->{'cities'}->{$cityId}->{transporters}->{sum} = $extractor->find('//span[@id="value_transSum"]/text()');
+    $self->{'cities'}->{$cityId}->{transporters}->{sum} =~ s/[\(|\)]//g;
+    $self->{'cities'}->{$cityId}->{maxActionPoints} = $extractor->find('//span[@id="value_maxActionPoints"]');;
+}
+
+sub checkAcademy {
+    my $self = shift;
+    my $cityId = shift;
+    my $building = "academy";
 
-    my $out = {};
-    foreach my $elem (@elems) {
-        my @items = getElementsByTagName($elem, "a");
-        foreach my $item (@items) {
-            if($item->getAttribute('href') =~ /view=researchDetail&id=\d+&position=\d+&researchId=(\d+)$/) {
-                @$out{$1} = $item->innerText();
+    my @locations = @{$self->{'cities'}->{$cityId}->{locations}};
+    # init
+    $self->{'cities'}->{$cityId}->{$building}->{'maxValue'} = 0;
+    $self->{'cities'}->{$cityId}->{$building}->{'iniValue'} = 0;
+    foreach (0..$#locations) {
+        if($locations[$_] eq $building) {
+            my $res = $self->{mech}->get(sprintf('http://%s/index.php?view=%s&id=%d&position=%d', $self->{server}, $building, $cityId, $_ ));
+            my $extractor = Ikariam::Extractor->new(content => $res->content);
+            if($extractor->{doc}->toString(0) =~ /maxValue : (\d+),\s+overcharge : \d+,\s+iniValue : (\d+),/) {
+                $self->{'cities'}->{$cityId}->{$building}->{'maxValue'} = $1;
+                $self->{'cities'}->{$cityId}->{$building}->{'iniValue'} = $2;
+            }
+            last;
+        }
+    }
+}
+
+sub checkTavern {
+    my $self = shift;
+    my $cityId = shift;
+
+    my @locations = @{$self->{'cities'}->{$cityId}->{locations}};
+    # init
+    $self->{'cities'}->{$cityId}->{'tavern'}->{'maxValue'} = 0;
+    $self->{'cities'}->{$cityId}->{'tavern'}->{'iniValue'} = 0;
+    foreach (0..$#locations) {
+        if($locations[$_] eq 'tavern') {
+            my $res = $self->{mech}->get(sprintf('http://%s/index.php?view=tavern&id=%d&position=%d', $self->{server}, $cityId, $_ ));
+            my $extractor = Ikariam::Extractor->new(content => $res->content);
+            if($extractor->{doc}->toString(0) =~ /maxValue : (\d+),\s+overcharge : \d+,\s+iniValue : (\d+),/) {
+                $self->{'cities'}->{$cityId}->{'tavern'}->{'maxValue'} = $1;
+                $self->{'cities'}->{$cityId}->{'tavern'}->{'iniValue'} = $2;
+            }
+            last;
+        }
+    }
+}
+
+sub checkMilitaryAdvisorMilitaryMovements  {
+    my $self = shift;
+    my $res = $self->{mech}->get(sprintf('http://%s/index.php?view=militaryAdvisorMilitaryMovements', $self->{server}));
+    my $extractor = new Ikariam::Extractor(content => $res->content);
+
+    $self->{'military'}->{wars} = 0;
+    $self->{'military'}->{attack} = 0;
+
+    foreach (qw/homeland elsewhere/) {
+        $self->{'military'}->{$_} = ();
+        my $result = $extractor->{doc}->find(sprintf('//div[@id="%s"]//ul[@id="battleReports"]/li[@class="enroute"]', $_));
+        foreach my $div ( @$result ) {
+            my $extractor = new Ikariam::Extractor(content => $div->toString(1));
+            my $f = $extractor->find('//div[@class="report"]/a[1]/@href');
+            my $t = $extractor->find('//div[@class="report"]/a[2]/@href');
+            $f = $1 if($f =~ /\?view=island&cityId=(\d+)/);
+            $t = $1 if($t =~ /\?view=island&cityId=(\d+)/);
+
+            push @{$self->{'military'}->{$_}}, { from => $f, to => $t};
+            if($_ eq 'homeland') {
+                $self->{'military'}->{wars}++;
+            } else {
+                $self->{'military'}->{attack}++;
             }
         }
     }
-    return $out;
+}
+
+sub checkMilitaryAdvisorReportView {
+    my $self = shift;
+    my $combatId = shift;
+    my $res = $self->{mech}->get(sprintf('http://%s/index.php?view=militaryAdvisorReportView&detailedCombatId=%s', $self->{server}, $combatId));
+    my $extractor = new Ikariam::Extractor(content => $res->content);
+
+    my %report;
+
+    $report{id} = $combatId;
+    my $c = $extractor->{doc}->toString(0);
+    
+    # FIXME 城鎮  6 級)的城牆( 2 級)為防禦部隊增加了 7% 的防禦力。
+    while($c =~ /城鎮\s+(\d+)/gs) {
+        $report{cityLevel} = $1;
+        $report{wallLevel} = $2;
+    }
+    
+    $report{attacker} = $extractor->find('//div[@id="troopsReport"]//table[@id="result"]//th[@class="attackers"]');
+    $report{defender} = $extractor->find('//div[@id="troopsReport"]//table[@id="result"]//th[@class="defenders"]');
+    $report{winner} = $extractor->find('//div[@id="troopsReport"]//table[@id="result"]//td[@class="winner"]');
+
+    # the combat we win!
+    $report{targetCity} = $extractor->find('//div[@id="troopsReport"]/div/h3/a/text()');
+    my $href = $extractor->find('//div[@id="troopsReport"]/div/h3/a/@href');
+    if(!defined($href)) {
+        # the combat we lost!
+        $report{targetCity} = $extractor->find('//td[@class="battle"]/a/text()');
+        $href = $extractor->find('//td[@class="battle"]/a/@href');
+    }
+
+    if($href =~ /index\.php\?view=island&id=(\d+)&selectCity=(\d+)/) {
+       $report{island} = $1;
+       $report{city} = $2;
+    } else {
+        warn($href);
+        warn ("can not read combat reprot $combatId");
+        return undef;
+    }
+    $report{gold} = $extractor->find('//div[@id="troopsReport"]//ul[@class="resources"]/li[@class="gold"]');
+    $report{wood} = $extractor->find('//div[@id="troopsReport"]//ul[@class="resources"]/li[@class="wood"]');
+    $report{crystal} = $extractor->find('//div[@id="troopsReport"]//ul[@class="resources"]/li[@class="glass"]');
+    $report{wine} = $extractor->find('//div[@id="troopsReport"]//ul[@class="resources"]/li[@class="wine"]');
+    $report{sulfur} = $extractor->find('//div[@id="troopsReport"]//ul[@class="resources"]/li[@class="sulfur"]');
+
+    foreach(qw/gold wood crystal wine sulfur attacker defender winner/) {
+        $report{$_} =~ s/^.*?:\s+//;
+        $report{$_} =~ s/\s+$//;
+        $report{$_} =~ s/,//g;
+    }
+    return \%report;
+}
+
+sub checkMilitaryAdvisorCombatReports {
+    my $self = shift;
+    my $page = shift || 0;
+
+    my $res = $self->{mech}->get(sprintf('http://%s/index.php?view=militaryAdvisorCombatReports&start=%s', $self->{server}, $page));
+    my $extractor = new Ikariam::Extractor(content => $res->content);
+
+    my $result = $extractor->{doc}->find('//div[@id="troopsOverview"]//table/tr[position()<=10]');
+    foreach my $tr ( @$result ) {
+        my $trExtractor = new Ikariam::Extractor(content => $tr->toString(1));
+        my $date = $trExtractor->find('//td[@class="date"]');
+        $date =~ s/^\s+//g; $date =~ s/\s+$//g;
+
+
+        my $href = $trExtractor->find('//td/a/@href');
+        if($href =~ /index.php\?view=militaryAdvisorReportView&combatId=(\d+)/) {
+            my $report = Ikariam::Report->retrieve($1);
+            if(!$report) {
+                my $report_hash = $self->checkMilitaryAdvisorReportView($1);
+                if(defined($report_hash)) {
+                    $report = Ikariam::Report->insert($report_hash);
+
+                    if($date =~ /(\d+)\.(\d+)\. (\d+):(\d+)/) {
+                        my $unixtime = mktime (0, $4, $3, $1, ($2-1), '108');
+                        $report->set('date', $unixtime);
+                    }
+                    $report->set('time', time);
+
+                    $report->update();
+                } else {
+                    # for some reason, we can not read the report.
+                    next;
+                }
+            } else {
+                # we have found the report we like to know, exit the function.
+                return;
+            }
+        } else {
+            # there is not report yet.
+            return;
+        }
+    }
+
+    my @moreCombats = $extractor->find('//div[@id="troopsOverview"]//table/tr[position()>10]//a/@href');
+    foreach (@moreCombats){
+        last if(/^javascript/);
+        if(/\?view=militaryAdvisorCombatReports&start=(\d+)/) {
+            next if($1 le $page);
+            $self->checkMilitaryAdvisorCombatReports($1);
+            last;
+        }
+    }
+}
+
+sub checkTownHall {
+    my $self = shift;
+    my $cityId = shift;
+
+    my $res = $self->{mech}->get(sprintf('http://%s/index.php?view=townHall&id=%d', $self->{server}, $cityId));
+    my $extractor = new Ikariam::Extractor(content => $res->content);
+
+    # //div[@id='SatisfactionOverview']//div[@class='value']
+    # //div[@id='SatisfactionOverview']//div[@class='text']
+    $self->{"cities"}->{$cityId}->{happiness} = $extractor->find('//div[@id="SatisfactionOverview"]//div[@class="value"]');
+
+    # 取 SatisfactionOverview 中的其他值
+    # 一個城鎮的市民滿意度結合了多方面的因素
+    # check happiness
+    # Happiness = Basic bonuses (196 + Capital Bonus + Holiday Bonus(25)) + 
+    #             Wine (Tavern Base(12*level) + 
+    #             Tavern Bonus(80*step)) + 
+    #             Culture (Museum Base(20*level) + 
+    #             Cultural Goods Bonus(50*Cultural Goods)) - 
+    #             Population (population) - 
+    #             Corruption (Corruption rate * population)
+    #
+    # Growth Rate = Happiness * 0.02
+
+    # Space, 房屋數
+    $self->{'cities'}->{$cityId}->{"space"} = {};
+    $self->{'cities'}->{$cityId}->{"space"}->{'total'} = $extractor->find('//span[@class="value total"]/text()');
+    $self->{'cities'}->{$cityId}->{"space"}->{'occupied'} = $extractor->find('//span[@class="value occupied"]/text()');
+
+    my @values = $extractor->find('//div[@id="CityOverview"]//span[@class="value"]');
+    $self->{'cities'}->{$cityId}->{"growth"} = $values[0];
+    $self->{'cities'}->{$cityId}->{"incomegold"} = $values[1];
+
+    my $actionPoints = $extractor->find('//div[@id="CityOverview"]//li[@class="actions"]/text()');
+    if($actionPoints =~ /(\d+)\/(\d+)/) {
+        $self->{'cities'}->{$cityId}->{actionPoints} = $1;
+        $self->{'cities'}->{$cityId}->{maxActionPoints} = $2;
+    }
+
+    $self->{'cities'}->{$cityId}->{corruption} = $extractor->find('//li[@class="corruption"]//span[@title="目前腐敗程度"]');
+    $self->{'cities'}->{$cityId}->{corruption} =~ s/%//g;
+
+    my @citizens_type = qw/citizens woodworkers specialworkers scientists/;
+    @values = $extractor->find('//div[@id="PopulationGraph"]//span[@class="count"]');
+    foreach my $i (0..$#citizens_type)
+    {
+        $self->{'cities'}->{$cityId}->{'citizens'}->{$citizens_type[$i]} = $values[$i];
+        $self->{'cities'}->{$cityId}->{'citizens'}->{total} += $values[$i];
+    }
+}
+
+sub checkArmies
+{
+    my $self = shift;
+    my $cityId = shift;
+    my %force_types;
+
+    $force_types{'army'} = [ qw/undef undef Slinger Swordsman Phalanx Ram Archer Catapult Gunsman Mortar SteamGiant Gyrocopter Bombardier Doctor Cook/ ];
+    $force_types{'fleet'} = [ qw/undef undef Ram-Ship BallistaShip Flamethrower CatapultShip MortarShip PaddleWheelRam DivingBoat/ ];
+
+    foreach my $x (qw/army fleet/) {
+        $self->{'cities'}->{$cityId}->{$x} = {};
+        my $res = $self->{mech}->get(sprintf('http://%s/index.php?view=cityMilitary-%s&id=%d', $self->{server}, $x, $cityId));
+        my @numbers = Ikariam::Extractor->new(content => $res->content)->find('//table//tr/td');
+        foreach my $j (0..$#{$force_types{$x}}) {
+            next if(!defined($force_types{$x}[$j]));
+            if ($numbers[$j] == '-') {
+                $self->{'cities'}->{$cityId}->{$x}->{$force_types{$x}[$j]} = 0;
+            } else {
+                $self->{'cities'}->{$cityId}->{$x}->{$force_types{$x}[$j]} = $numbers[$j];
+            }
+        }
+    }
+}
+
+sub checkFriends 
+{
+    # must check cities first, so we know if we have a museum available.
+    my $self = shift;
+
+    foreach my $cityId (keys(%{$self->{'cities'}})) {
+        my @locations = @{$self->{'cities'}->{$cityId}->{locations}};
+        foreach (0..$#locations) {
+            if($locations[$_] eq 'museum') {
+                my $res = $self->{mech}->get(sprintf('http://%s/index.php?view=museum&id=%d&position=%d', $self->{server}, $cityId, $_ ));
+                my @hrefs = Ikariam::Extractor->new(content => $res->content)->find('//div[@class="content"]/table/tbody/tr/td/a[1]/@href');
+                foreach my $href (@hrefs) { 
+                    if ($href =~ /&id=(\d+)&/) {
+                        $self->{'friends'}->{$1} = undef;
+                    }
+                }
+                last;
+            }
+        }
+    }
+
+    if(-f "friends.txt") {
+        # load friends
+        open(IN, "friends.txt") or die "Unable to open friends.txt\n";
+        while(<IN>) {
+            chomp;
+            my $friend = Ikariam::User->retrieve(name => $_);
+            $self->{friends}->{$friend->id} = undef if(defined($friend));
+        }
+        close(IN);
+    }
 }
 
 sub check
 {
     my $self = shift;
 
-    # looking for cities
-    foreach my $cityId (keys(%{$self->{'cities'}})) 
-    {
-        # search for goods
-        my $res = $self->{mech}->post(sprintf('http://%s/index.php', $self->{server}), [
-            action => 'header',
-            cityId => $cityId,
-            function => 'changeCurrentCity',
-            id => $cityId,
-            oldView => 'city',
-        ]);
+    # MilitaryAdvisor
+    $self->checkMilitaryAdvisorMilitaryMovements();
+    $self->checkMilitaryAdvisorCombatReports();
+    $self->{'research'} = $self->checkResearch((keys(%{$self->{'cities'}}))[0]);
+
+    # alerts
+    # //li[@id="advCities"]/a[@class="normalalert"]
+    # //li[@id="advMilitary"]/a[@class="normalalert"]
+    # //li[@id="advResearch"]/a[@class="normalalert"]
+    # //li[@id="advDiplomacy"]/a[@class="normalalert"]
 
-        my $content;
-        gunzip \$res->content => \$content 
-            or die "gunzip failed: $GunzipError\n";
-
-        my $html = HTML::TagParser->new($content);
-        my @elems;
+    # looking for cities
+    foreach my $cityId (keys(%{$self->{'cities'}})) {
+        $self->checkCity($cityId);
+        $self->checkTownHall($cityId);
+        $self->checkArmies($cityId);
+        $self->checkTavern($cityId);
+        $self->checkAcademy($cityId);
+    }
+    # $self->checkFriends();
+    return $self->{'cities'};
+}
 
-        my ($elem) = $html->getElementsByAttribute("id", "value_gold");
-        $self->{'cities'}->{$cityId}->{resources}->{gold} = $elem->innerText();
-        $self->{'cities'}->{$cityId}->{resources}->{gold} =~ s/,//g;
-
-        my ($elem) = $html->getElementsByAttribute("class", "city");
-        $self->{'cities'}->{$cityId}->{name} = $elem->innerText();
-
-        my ($elem) = $html->getElementsByAttribute("class", 'constructionSite');
-        $self->{'cities'}->{$cityId}->{construction} = 0;
-        $self->{'cities'}->{$cityId}->{construction} = 1 if(defined($elem));
+sub getNetIncome {
+    my $self = shift;
+    my $cityId = shift;
 
-        # check goods
-        foreach my $good (qw/wood wine marble crystal sulfur/) {
-            my ($elem) = $html->getElementsByAttribute("id", "value_" . $good);
-            $self->{'cities'}->{$cityId}->{resources}->{$good} = $elem->innerText();
-            $self->{'cities'}->{$cityId}->{resources}->{$good} =~ s/,//g;
-        }
+    Carp::croak("must assign cityId") if !defined($cityId);
 
-        # search locations
-        foreach my $i (0..14) {
-            my ($elem) = $html->getElementsByAttribute("id", "position" . $i);
-            my $building = $elem->getAttribute('class');
-            if (!($building =~ /buildingGround/)) {
-                $self->{'cities'}->{$cityId}->{locations}[$i] = $building;
-                my $span = getElementsByAttribute($elem, "class", "textLabel");
-                my (undef, undef, $level) = split(/ /, $span->innerText());
-                $self->{'cities'}->{$cityId}->{buildings}->{$building} = $level;
-            }
-        }
+    # 扣除研發每人花費 8 GOLD
+    return ($self->{cities}->{$cityId}->{citizens}->{total}*4 - $self->{cities}->{$cityId}->{citizens}->{scientists}*8 );
+}
+
+sub getNavyExpenditure {
+    my $self = shift;
+    my $cityId = shift;
+    my $cities = $self->{cities};
 
-        # transporters
-        my ($elem) = $html->getElementsByAttribute("class", 'transAvail');
-        $self->{'cities'}->{$cityId}->{transporters}->{avail} = $elem->innerText();
-        my ($elem) = $html->getElementsByAttribute("class", 'transSum');
-        $self->{'cities'}->{$cityId}->{transporters}->{sum} = $elem->innerText();
-        $self->{'cities'}->{$cityId}->{transporters}->{sum} =~ s/\(//;
-        $self->{'cities'}->{$cityId}->{transporters}->{sum} =~ s/\)//;
-
-        $res = $self->{mech}->get(sprintf('http://%s/index.php?view=militaryAdvisorMilitaryMovements', $self->{server}, $cityId));
-        gunzip \$res->content => \$content 
-            or die "gunzip failed: $GunzipError\n";
-
-        $self->{'cities'}->{$cityId}->{force}->{attacks} = $1 if($content =~ /敵人攻擊: (\d+)/);
-        $self->{'cities'}->{$cityId}->{force}->{wars} = $1 if($content =~ /我方軍隊行程: (\d+)/);
-        # if($content =~ /更新戰鬥報告: (\d+)/);
-        # if($content =~ /新的戰鬥報告: (\d+)/);
-
-        # sub checkTownHall {
-        $res = $self->{mech}->get(sprintf('http://%s/index.php?view=townHall&id=%d', $self->{server}, $cityId));
-        gunzip \$res->content => \$content 
-            or die "gunzip failed: $GunzipError\n";
+    my $ships = {
+        CargoShip => {n => "CargoShip",a => 0,d => 0,s => 4,c => "Steamship",v => 20,A => 0,D => 0},
+        'Ram-Ship' => {n => "Ram-Ship",p => 6,w => 56,S => 21,b => "34m",u => 20,m => 1,o => 3,a => 16,d => 16,A => 4,D => 4,s => 10,c => "Sailer",v => 10},
+        BallistaShip => {n => "BallistaShip",p => 5,w => 72,S => 29,b => "47m",u => 24,m => 3,o => 5,a => 20,d => 28,A => 5,D => 7,s => 11,c => "Sailer",v => 8,x => "Resistance"},
+        Flamethrower => {n => "Flamethrower",p => 5,w => 105,S => 77,b => "1h 55m",u => 45,m => 5,o => 7,a => 40,d => 40,A => 10,D => 10,s => 12,c => "Steamship",v => 8,x => "Assault"},
+        CatapultShip => {n => "CatapultShip",p => 10,w => 173,S => 76,b => "3h 11m",u => 57,m => 7,o => 10,a => 60,d => 60,A => 12,D => 12,s => 16,c => "Sailer",v => 6},
+        MortarShip => {n => "MortarShip",p => 22,w => 456,S => 282,b => "3h 38m",u => 130,m => 12,o => 15,a => 160,d => 160,A => 35,D => 35,s => 14,c => "Steamship",v => 4},
+        PaddleWheelRam => {n => "PaddleWheelRam",i => "steamboat",p => 12,w => 513,S => 167,b => "4h 8m",u => 114,m => 10,o => 13,a => 100,d => 90,A => 20,D => 18,s => 13,c => "Steamship",v => 8,x => "Assault"},
+        DivingBoat => {n => "DivingBoat",i => "submarine",p => 16,w => 493,C => 378,b => "5h 5m",u => 126,m => 15,o => 16,a => 110,d => 155,A => 20,D => 30,s => 10,c => "Steamship",v => 2,x => "Resistance"}
+    };
+    my $cost = 0;
+    foreach(keys(%{$cities->{$cityId}->{fleet}})) {
+        $cost += $cities->{$cityId}->{fleet}->{$_} * $ships->{$_}->{u};
+    }
+    return $cost;
+}
 
-        # check happiness
-        # Happiness = Basic bonuses (196 + Capital Bonus + Holiday Bonus(25)) + 
-        #             Wine (Tavern Base(12*level) + 
-        #             Tavern Bonus(80*step)) + 
-        #             Culture (Museum Base(20*level) + 
-        #             Cultural Goods Bonus(50*Cultural Goods)) - 
-        #             Population (population) - 
-        #             Corruption (Corruption rate * population)
-        #
-        # Growth Rate = Happiness * 0.02
-        $html = HTML::TagParser->new($content);
-
-        my @happiness = ("ecstatic", "happy", "neutral", "sad", "outraged");
-        foreach my $j (0..$#happiness) {
-            my ($elem) = $html->getElementsByAttribute("class", sprintf("happiness happiness_%s", $happiness[$j]));
-            if(defined($elem)) {
-                $self->{'cities'}->{$cityId}->{happiness} = $j;
-                $self->{'cities'}->{$cityId}->{happiness_text} = $happiness[$j];
-            }
-        }
+sub getMilityExpenditure {
+    my $self = shift;
+    my $cityId = shift;
+    my $cities = $self->{cities};
+    my $troops = {
+        # p 需要人口
+        # w 木頭
+        # S 硫磺
+        # b 製造時間
+        # u 維持費用
+        # m 最低軍營等級
+        # o 最佳軍營等級
+        # a 攻擊力
+        # d 防守力
+        # A 攻擊力加成
+        # D 防守力加成
+        # s 耐力
+        # c 種類
+        # v Speed
+        # x 攻防加成 Assault, Resistance
+        Slinger => {n => "Slinger", p => 1, w => 40, b => "12m", u => 8, m => 1, o => 4, a => 7, d => 6, A => 2, D => 2, s => 10, c => "Human", v => 20}, 
+        Swordsman => {n => "Swordsman", p => 2, w => 47, S => 16, b => "17m", u => 16, m => 3, o => 5, a => 18, d => 14, A => 4, D => 3, s => 12, c => "Human", v => 20, x => "Assault"}, 
+        Phalanx => {n => "Phalanx", p => 4, w => 104, S => 64, b => "40m", u => 24, m => 4, o => 7, a => 24, d => 40, A => 6, D => 10, s => 14, c => "Human", v => 20, x => "Resistance"}, 
+        Ram => {n => "Ram", p => 8, w => 198, S => 128, b => "42m", u => 52, m => 5, o => 8, a => 14, d => 18, A => 3, D => 4, s => 16, c => "Machina", v => 20, x => "Ram"}, 
+        Archer => {n => "Archer", p => 4, w => 172, S => 86, b => "49m", u => 32, m => 7, o => 10, a => 40, d => 40, A => 10, D => 10, s => 12, c => "Human", v => 20}, 
+        Catapult => {n => "Catapult", p => 10, w => 342, S => 232, b => "49m", u => 72, m => 10, o => 14, a => 36, d => 28, A => 9, D => 7, s => 16, c => "Machina", v => 20, x => "Ram"}, 
+        Gunsman => {n => "Gunsman", i => "marksman", p => 7, w => 355, S => 154, b => "1h 23m", u => 58, m => 14, o => 18, a => 80, d => 64, A => 18, D => 14, s => 10, c => "Human", v => 20}, 
+        Mortar => {n => "Mortar", p => 12, w => 1325, S => 938, b => "1h 53m", u => 128, m => 19, o => 21, a => 64, d => 64, A => 15, D => 15, s => 16, c => "Machina", v => 20, x => "Ram"}, 
+        SteamGiant => {n => "SteamGiant", i => "steamgiant", p => 6, w => 1150, S => 716, b => "1h 45m", u => 68, m => 16, o => 20, a => 100, d => 140, A => 20, D => 30, s => 14, c => "Machina", v => 20, x => "Resistance"}, 
+        Gyrocopter => {n => "Gyrocopter", p => 8, w => 1250, S => 670, b => "1h 2m", u => 97, m => 12, o => 16, a => 112, d => 112, A => 25, D => 25, s => 12, c => "Machina", v => 20}, 
+        Bombardier => {n => "Bombardier", p => 24, w => 2270, S => 878, b => "2h 10m", u => 228, m => 22, o => 24, a => 200, d => 165, A => 45, D => 35, s => 14, c => "Machina", v => 20, x => "Assault"}, 
+        Doctor => {n => "Doctor", i => "medic", p => 6, w => 640, C => 361, b => "1h 2m", u => 244, m => 11, o => 12, a => 4, d => 28, A => 0, D => 0, s => 14, c => "Human", v => 20, x => "Healer"}, 
+        Cook => {n => "Cook", p => 4, w => 520, W => 103, b => "38m", u => 138, m => 8, o => 8, a => 6, d => 26, A => 0, D => 0, s => 16, c => "Human", v => 20, x => "Regeneration"}
+    };
 
-        # Space, 房屋數
-        $self->{'cities'}->{$cityId}->{"space"} = {};
-        foreach my $j (qw/occupied total/) {
-            my ($elem) = $html->getElementsByAttribute("class", sprintf("value %s", $j));
-            if(defined($elem)) {
-                $self->{'cities'}->{$cityId}->{"space"}->{$j} = $elem->innerText();
-            }
-        }
-
-        # Actions
-        # <span id="value_maxActionPoints">1</span>
-
-        # <li class="incomegold incomegold_negative">
-        # <li class="incomegold incomegold_positive">
-        # <span class="value">-178</span>
-
-        my ($elem) = $html->getElementsByAttribute("title", "目前腐敗程度");
-        if(defined($elem)) {
-            $self->{'cities'}->{$cityId}->{corruption} = $elem->innerText();
-            $self->{'cities'}->{$cityId}->{corruption} =~ s/%//g;
-        }
+    my $cost = 0;
+    foreach(keys(%{$cities->{$cityId}->{army}})) {
+        $cost += $cities->{$cityId}->{army}->{$_} * $troops->{$_}->{u};
+    }
+    return $cost;
+}
 
-        # countCiizens
-        my @citizens_type = qw/citizens woodworkers specialworkers scientists/;
-        @elems = $html->getElementsByAttribute('class', 'count');
-        $self->{'cities'}->{$cityId}->{'citizens'} = {};
-        $self->{'cities'}->{$cityId}->{'citizens'}->{total} = 0;
+sub blanceHurmanResource {
+    my $self = shift;
+    my $cityId = shift;
+    my $workersRatio = {
+        'citizens' => 0.4,
+        'specialworkers' => 0.3,
+        'woodworkers' => 0.7,
+    };
 
-        foreach my $i (0..$#citizens_type)
-        {
-            $self->{'cities'}->{$cityId}->{'citizens'}->{$citizens_type[$i]} = $elems[$i]->innerText();
-            $self->{'cities'}->{$cityId}->{'citizens'}->{total} += $elems[$i]->innerText();;
-        }
+    my $netincome = $self->getNetIncome($cityId);
 
-        # } 
-        
-        $self->{'cities'}->{$cityId}->{'research'} = $self->checkResearch($cityId);
+    # --- HR ---
+    # 扣除研發,四成種田生產,剩下 3:7 挖資源
+    # 四成收入中可用兩成做軍事用途
+    # 生產共四成 
+    my $produceworkers = int(($netincome * $workersRatio->{citizens}) / 4);
+
+    # 換成生產人力
+    my $freePeople = $self->{cities}->{$cityId}->{citizens}->{total} - ($produceworkers + $self->{cities}->{$cityId}->{scientists});
 
-        # sub checkArmies {
-        my %force_types;
-        $force_types{'army'} = [ qw/undef undef Slinger Swordsman Phalanx Ram Archer Catapult Gunsman Mortar SteamGiant Gyrocopter Bombardier Doctor Cook/ ];
-        $force_types{'fleet'} = [ qw/undef undef Ram-Ship BallistaShip Flamethrower CatapultShip MortarShip PaddleWheelRam DivingBoat/ ];
-        foreach my $x (qw/army fleet/)
-        {
-            $self->{'cities'}->{$cityId}->{$x} = {};
-            # search army
-            $res = $self->{mech}->get(sprintf('http://%s/index.php?view=cityMilitary-%s&id=%d', $self->{server}, $x, $cityId));
-            gunzip \$res->content => \$content 
-                or die "gunzip failed: $GunzipError\n";
+    # XXX
+    # 需計算資源開挖上限, 依照島等級區分
+    # 木頭
+#   create_slider({
+#            dir : 'ltr',
+#            id : "default",
+#            maxValue : 367,
+#            overcharge : 0,
+#            iniValue : 367,
+#            bg : "sliderbg",
+#            thumb : "sliderthumb",
+#            topConstraint: -10,
+#            bottomConstraint: 344,
+#            bg_value : "actualValue",
+#            bg_overcharge : "overcharge",
+#            textfield:"inputWorkers"
+#    });
 
-            $html = HTML::TagParser->new($content);
-            @elems = $html->getElementsByTagName('td');
-            foreach my $j (0..$#{$force_types{$x}}) {
-                next if($force_types{$x}[$j] eq 'undef');
-                if($elems[$j]->innerText() == '-') {
-                    $self->{'cities'}->{$cityId}->{$x}->{$force_types{$x}[$j]} = 0;
-                } else {
-                    $self->{'cities'}->{$cityId}->{$x}->{$force_types{$x}[$j]} = $elems[$j]->innerText();
-                }
-            }
-        }
-    }
-    # print Dumper($self->{'cities'});
-    return $self->{'cities'};
+#    	create_slider({
+#            dir : 'ltr',
+#            id : "default",
+#			maxValue : 367,
+#			overcharge : 0,
+#			iniValue : 367,
+#			bg : "sliderbg",
+#			thumb : "sliderthumb",
+#			topConstraint: -10,
+#			bottomConstraint: 344,
+#			bg_value : "actualValue",
+#			bg_overcharge : "overcharge",
+#			textfield:"inputWorkers"
+#	});
+
+    Carp::carp(sprintf("Suggested HR palnning - produce: %s wood %s special %s\n", $produceworkers, int($freePeople*0.3), int($freePeople*0.7)));
 }
 
 sub logout
@@ -543,62 +1191,17 @@
         name => $self->{user},
         password => $self->{pass},
         ]);
-    my $c;
-    my $status = gunzip \$res->content => \$c 
-        or die "gunzip failed: $GunzipError\n";
+
+    my @cities = Ikariam::Extractor->new(content => $res->content)->find('//option[@class="avatarCities coords"]/@value');
 
-    if($c =~ /錯誤!/)
-    {
-        die ("password error\n");
-    } else {
-        my $html = HTML::TagParser->new($c);
-        my @elems;
-        
-        @elems = $html->getElementsByAttribute("class", "avatarCities coords");
-        foreach my $elem (@elems) {
-            # my cities
-            $self->{'cities'}->{$elem->getAttribute('value')} = {};
-        }
+    if($#cities<0) {
+        die ("login failed\n");
+    }
+    foreach(@cities) {
+        $self->{'cities'}->{$_} = {};
+        $self->{'cities'}->{$_}->{id} = $_;
     }
 }
 
-sub getElementsByTagName {
-    my $element = shift;
-    my $tagname = lc(shift);
-    my ( $flat, $cur ) = @$element;
-
-    my $out = [];
-    for( ; $cur <= $#$flat ; $cur++ ) {
-        last if ($flat->[ $cur + 1 ]->[001] eq $element->tagName() );
-        next if ($flat->[$cur]->[001] ne $tagname );
-        next if $flat->[$cur]->[000]; # close
-
-        my $elem = HTML::TagParser::Element->new( $flat, $cur );
-        return $elem unless wantarray;
-        push( @$out, $elem );
-    }
-    return unless wantarray;
-    @$out;
-}
-
-sub getElementsByAttribute {
-    my $element = shift;
-    my $key  = lc(shift);
-    my $val  = shift;
-    my ( $flat, $cur ) = @$element;
-
-    my $out  = [];
-    for ( ; $cur <= $#$flat ; $cur++ ) {
-        next if $flat->[$cur]->[000];    # close
-        my $elem = HTML::TagParser::Element->new( $flat, $cur );
-        my $attr = $elem->attributes();
-        next unless exists $attr->{$key};
-        next if ( $attr->{$key} ne $val );
-        return $elem unless wantarray;
-        push( @$out, $elem );
-    }
-    return unless wantarray;
-    @$out;
-}
 
 1;
--- a/README	Thu Oct 23 00:47:56 2008 +0800
+++ b/README	Mon Dec 01 15:32:44 2008 +0800
@@ -21,7 +21,7 @@
 Class::DBI - libclass-dbi-perl
 Class::DBI::SQLite - libclass-dbi-sqlite-perl
 Decision::ParseTree - libdecision-parsetree-perl
-HTML::TagParser; - libhtml-tagparser-perl
+XML::LibXML - libxml-libxml-perl
 IO::Uncompress::Gunzip - libio-compress-zlib-perl
 LWP - libwww-perl
 WWW::Mechanize - libwww-mechanize-perl
@@ -40,7 +40,7 @@
 
 1;
 
- * Run the bot, 'perl agent.pl'
+ * Run the bot, 'perl agent.pl' (mark the 'eval' call in triggerAction for avoid action triggered by the bot.)
  * Update the islands information by runing 'perl scan.pl'
  * List sheeps, 'perl sheep.pl' (run scan.pl first)
 
--- a/agent.pl	Thu Oct 23 00:47:56 2008 +0800
+++ b/agent.pl	Mon Dec 01 15:32:44 2008 +0800
@@ -4,21 +4,24 @@
 use Ikariam;
 use Data::Dumper;
 use Decision::ParseTree q{ParseTree};
-use YAML qw/LoadFile Dump/;
+use YAML qw/LoadFile Dump DumpFile/;
 
 package Ikariam::Cities::Rules;
 use strict;
 use Data::Dumper;
 
 sub new {
-    my ( $class ) = @_;
-    my $self = {};
+    my ( $class, $i ) = @_;
+    my $self = {
+        ikariam => $i,
+    };
     return bless $self, $class;
 }
 
 sub is_attacked {
     my ($self, $city) = @_;
-    return ($city->{force}->{attacks} > 0 ) ? 1 : 0;
+
+    return ($self->{'ikariam'}->{'military'}->{attacks} > 0 ) ? 1 : 0;
 }
 
 sub is_constructing {
@@ -28,38 +31,112 @@
 
 sub is_wall_enough {
     my ($self, $city) = @_;
-    # TODO 應該以防禦力計算
+    # http://ikariam.wikia.com/wiki/Wall_Defense
+    # Basic rule - the wall level must equale or large then townHall level.
     return ($city->{buildings}->{wall} >= $city->{buildings}->{townHall} ?  1 : 0);
 }
 
+sub is_academy_enough {
+    my ($self, $city) = @_;
+    return ($city->{buildings}->{academy} >= 6 ?  1 : 0);
+}
+
+sub is_embassy_enough {
+    my ($self, $city) = @_;
+    return ($city->{buildings}->{embassy} >= 6 ?  1 : 0);
+}
+
+sub is_museum_enough {
+    my ($self, $city) = @_;
+    return ($city->{buildings}->{museum} >= ($city->{buildings}->{tavern}/2) ? 1 : 0);
+}
+
+sub is_branchOffice_enough {
+    my ($self, $city) = @_;
+    return ($city->{buildings}->{branchOffice} >= 6 ?  1 : 0);
+}
+
 sub is_space_enough {
     my ($self, $city) = @_;
-    # TODO 應該以消耗率/時間計算
-    return ($city->{space}->{total} <= ($city->{space}->{occupied}+6) ?  0 : 1)
+    # The maximum town hall is level 20, then we build new town
+    return 1 if($city->{buildings}->{townHall} >= 20);
+    # TODO 應該以 成長率 * 升級所需時間計算
+    # 6 hours earlier, we upgrade the twonHall.
+    return ($city->{space}->{total} <= ($city->{space}->{occupied} + ($city->{growth}*12)) ?  0 : 1)
+}
+
+sub is_safehouse_enough {
+    my ($self, $city) = @_;
+    return 0 if(!defined($city->{buildings}->{safehouse}));
+
+    # build the higgest safehouse.
+    # maybe we should have more then 4 towns, then we consider that we should upgrade safehouse at level 20.
+    # return (($city->{buildings}->{townHall} > $city->{buildings}->{safehouse}) ? 0 : 1) 
+        # if($$city->{buildings}->{townHall} >= 20);
+    # return (($city->{buildings}->{townHall} <= ($city->{buildings}->{safehouse} + 4)) ? 0 : 1);
+
+    # Safehouse must be same level as townHall,
+    # Maybe one level higher is better.
+    return (($city->{buildings}->{townHall} > $city->{buildings}->{safehouse}) ? 0 : 1);
+}
+
+sub is_warehouse_enough {
+    my ($self, $city) = @_;
+    # my @warehouse = (qw/undef undef 0 4 9 16 18 19 20 21 22 23 24 25/);
+    my @warehouse = (qw/undef undef 0 4 9 15 18 19 20 21 22 23 24 25/);
+    my @cities = keys(%{$self->{ikariam}->{cities}});
+    my $nextCities = ($#cities + 1) + 1;
+
+    Carp::carp(sprintf("Required warehouse level %s for next city (%s), current is %s\n", $warehouse[$nextCities], $nextCities, $city->{buildings}->{warehouse}));
+    return 0 if(!defined($city->{buildings}->{warehouse}));
+    return ($city->{buildings}->{warehouse} >= $warehouse[$nextCities]) ? 1 : 0;
+}
+
+
+sub is_warehouse_enougn_for_governorsresidence {
+    my ($self, $city) = @_;
+    my @warehouse = (qw/undef undef 0 4 9 16 18 19 20 21 22 23 24 25/);
+    my @cities = keys(%{$self->{ikariam}->{cities}});
+    my $citiesNumber = $#cities + 1;
+
+    Carp::carp(sprintf("Required warehouse level %s for %s cities, current is %s\n", $warehouse[$citiesNumber], $citiesNumber, $city->{buildings}->{warehouse}));
+    return 0 if(!defined($city->{buildings}->{warehouse}));
+    return ($city->{buildings}->{warehouse} >= $warehouse[$citiesNumber]) ? 1 : 0;
 }
 
 sub is_corruption {
     my ($self, $city) = @_;
+    return ($city->{corruption} > 0) ? 1 : 0;
+}
+
+sub is_any_corruption {
+    my ($self, $city) = @_;
+
+    foreach (keys(%{$self->{ikariam}->{cities}})) {
+        return 1 if ($self->{ikariam}->{cities}->{$_}->{corruption} > 0);
+    }
     return 0;
 }
 
 sub is_happiness {
     my ($self, $city) = @_;
-    # TODO 以 fuzzy 取出合理 happiness 值
-    return ($city->{happiness} >= 2 ?  1 : 0)
+
+    return ($city->{growth} >= 5 ?  1 : 0) 
+        if($city->{buildings}->{townHall} <= 16);
+
+    return 1 if($city->{buildings}->{townHall} >= 20);
+ 
+    return ($city->{happiness} >= 2 ?  1 : 0);
 }
 
-sub is_warehouse_enough {
+sub is_tavern_available {
     my ($self, $city) = @_;
-    # TODO 以速率計算容納率
-    # XXX: not implemented yet.
-    return 1;
+    return (defined($city->{buildings}->{tavern}) ? 1 : 0);
 }
 
-sub is_risk {
+sub is_bacchanal {
     my ($self, $city) = @_;
-    # TODO 計算可搶劫比例, 城牆防護, 軍事分數
-    return 0;
+    return ($city->{tavern}->{maxValue} == $city->{tavern}->{iniValue} ? 1 : 0);
 }
 
 sub is_gold_enoughforcargo {
@@ -88,81 +165,221 @@
 
 sub is_expansion_researched {
     my ($self, $city) = @_;
-    return (defined($city->{research}->{1030}) ?  1 : 0);
+    # $self->{'ikariam'}->
+    return (defined($self->{'ikariam'}->{research}->{1030}) ?  1 : 0);
+}
+
+sub is_foreigncultures_researched {
+    my ($self) = @_;
+    return (defined($self->{'ikariam'}->{research}->{1040}) ?  1 : 0);
+}
+
+sub is_greekfire_researched {
+    my ($self) = @_;
+    return (defined($self->{'ikariam'}->{research}->{1060}) ?  1 : 0);
+}
+
+sub is_conservation_researched {
+    my ($self) = @_;
+    return (defined($self->{'ikariam'}->{research}->{2010}) ?  1 : 0);
 }
 
 sub is_wealth_researched {
-    my ($self, $city) = @_;
-    return (defined($city->{research}->{2030}) ?  1 : 0);
+    my ($self) = @_;
+    return (defined($self->{'ikariam'}->{research}->{2030}) ?  1 : 0);
 }
 
-sub is_professionalarmy_researched {
-    my ($self, $city) = @_;
-    return (defined($city->{research}->{4030}) ?  1 : 0);
+sub is_winepress_researched {
+    my ($self) = @_;
+    return (defined($self->{'ikariam'}->{research}->{2040}) ?  1 : 0);
 }
 
 sub is_paper_researched {
-    my ($self, $city) = @_;
-    return (defined($city->{research}->{3020}) ?  1 : 0);
+    my ($self) = @_;
+    return (defined($self->{'ikariam'}->{research}->{3020}) ?  1 : 0);
+}
+
+sub is_espionage_researched {
+    my ($self) = @_;
+    return (defined($self->{'ikariam'}->{research}->{3030}) ?  1 : 0);
+}
+
+sub is_invention_researched {
+    my ($self) = @_;
+    return (defined($self->{'ikariam'}->{research}->{3040}) ?  1 : 0);
+}
+
+sub is_culturalexchange_researched {
+    my ($self) = @_;
+    return (defined($self->{'ikariam'}->{research}->{3060}) ?  1 : 0);
+}
+
+sub is_professionalarmy_researched {
+    my ($self) = @_;
+    return (defined($self->{'ikariam'}->{research}->{4030}) ?  1 : 0);
 }
 
 sub is_drydock_researched {
-    my ($self, $city) = @_;
-    return (defined($city->{research}->{4010}) ?  1 : 0);
-}
-
-sub is_winepress_researched {
-    my ($self, $city) = @_;
-    return (defined($city->{research}->{2040}) ?  1 : 0);
-}
-
-sub is_invention_researched {
-    my ($self, $city) = @_;
-    return (defined($city->{research}->{3040}) ?  1 : 0);
+    my ($self) = @_;
+    return (defined($self->{'ikariam'}->{research}->{4010}) ?  1 : 0);
 }
 
 sub is_barracks_level_enough {
     my ($self, $city) = @_;
-    return 0 if(!defined($city->{building}->{barracks}));
-    return ($city->{building}->{barracks} >= 3 ? 1 : 0);
+    return 0 if(!defined($city->{buildings}->{barracks}));
+    if($city->{buildings}->{townHall} >= 10) {
+        # optimum is 5
+        return ($city->{buildings}->{barracks} >= 5 ? 1 : 0);
+    } else {
+        # 方陣兵需要 level 4
+        return ($city->{buildings}->{barracks} >= 4 ? 1 : 0);
+    }
 }
 
 sub is_shipyard_level_enough {
     my ($self, $city) = @_;
-    return 0 if(!defined($city->{building}->{shipyard}));
-    return ($city->{building}->{shipyard} >= 2 ? 1 : 0);
+    return 0 if(!defined($city->{buildings}->{shipyard}));
+    if ($self->is_greekfire_researched() eq 1) {
+        return ($city->{buildings}->{shipyard} >= 5 ? 1 : 0);
+    } else {
+        return ($city->{buildings}->{shipyard} >= 3 ? 1 : 0);
+    }
+}
+
+sub is_shipyard_upgrading {
+    my ($self, $city) = @_;
+    return $self->{'ikariam'}->is_shipyard_upgrading($city->{id});
+}
+
+sub is_navy_trainning {
+    my ($self, $city) = @_;
+    return $self->{'ikariam'}->is_navy_trainning($city->{id});
+}
+
+sub train_navy {
+    my ($self, $city) = @_;
+    my $cityId = $city->{id};
+    # TODO, 依照升級比例算 CP 值最高
+    if($self->is_greekfire_researched() eq 1 && $city->{buildings}->{shipyard} >= 5) {
+        # ok, we can build Flamethrower.
+        if(defined($city->{'fleet'}->{Flamethrower}) && ($city->{'fleet'}->{Flamethrower} > 0)
+                && ($city->{'fleet'}->{BallistaShip} / $city->{'fleet'}->{Flamethrower}) <= (1.5/1)) {
+            return 213; # 強弩船
+        } else {
+            return 211; # 噴火船
+        }
+    } else {
+        return 213; # 強弩船
+    }
+}
+
+sub is_barracks_upgrading {
+    my ($self, $city) = @_;
+    return $self->{ikariam}->is_barracks_upgrading($city->{id});
+}
+
+sub is_army_trainning {
+    my ($self, $city) = @_;
+    return $self->{ikariam}->is_army_trainning($city->{id});
+}
+sub train_army {
+    my ($self, $city) = @_;
+    my $cityId = $city->{id};
+
+    # TODO, 依照升級比例算 CP 值最高
+    my $assault = 'Swordsman';
+    my $resistance = 'Phalanx';
+
+    if(($city->{'army'}->{$assault} / $city->{'army'}->{$resistance}) <= (2/1)) {
+        return 302; # Swordsman
+    } else {
+        return 303; # Phalanx
+    }
+}
+
+# navy
+sub is_navyExpenditure_available
+{
+    my ($self, $city) = @_;
+    my $cityId = $city->{id};
+
+    # move this to somewhere else.
+    my $workersRatio = {
+        'citizens' => 0.4,
+        'specialworkers' => 0.3,
+        'woodworkers' => 0.7,
+    };
+
+    my $currentCost = $self->{'ikariam'}->getNavyExpenditure($cityId);
+    my $netincome = $self->{'ikariam'}->getNetIncome($cityId);
+    
+    # 軍費為 兩成 淨收入
+    # 陸軍佔用 0.3
+    # 海軍佔用 0.7
+    my $militaryExpenditure = int($netincome * 0.2 * 0.7);
+
+    if($currentCost < $militaryExpenditure) {
+        printf("Current navy expenditure total=%s, affordable %s\n", $currentCost, $militaryExpenditure);
+        return 1;
+    }
     return 0;
 }
 
-sub rule_engagement
+# army
+sub is_milityExpenditure_available
 {
     my ($self, $city) = @_;
-    # XXX
-    # 計算距離, 可搶劫比例, 是否有聯盟, 軍事分數 (win rate)
+    my $cityId = $city->{id};
+
+    # move this to somewhere else.
+    my $workersRatio = {
+        'citizens' => 0.4,
+        'specialworkers' => 0.3,
+        'woodworkers' => 0.7,
+    };
+
+    my $currentCost = $self->{'ikariam'}->getMilityExpenditure($cityId);
+    my $netincome = $self->{'ikariam'}->getNetIncome($cityId);
+    
+    # 軍費為 兩成 淨收入
+    # 陸軍佔用 0.3
+    # 海軍佔用 0.7
+
+    my $militaryExpenditure = int($netincome * 0.2 * 0.3);
+
+    if($currentCost < $militaryExpenditure) {
+        Carp::carp("Current army expenditure total=%s, affordable %s\n", $currentCost, $militaryExpenditure);
+        return 1;
+    }
+    return 0;
 }
 
-sub rule_resource
-{
-    # XXX
-}
 1; 
 
 
 package main;
+my $verbose = 1;
+
 our $i = new Ikariam($::server, $::user, $::pass);
 $i->login;
 my $cities = $i->check;
 
-my $rules = Ikariam::Cities::Rules->new;
-my $tree  = LoadFile('building.yaml');
-# print Dumper($tree);
+# Genereic rules for both overall and city level arranagement.
+my $rules = Ikariam::Cities::Rules->new($i);
+# blanace resources, arrange defance 
+my $tree  = LoadFile('overall.yaml');
+my $decision; $decision->{parse_path} = []; $decision->{parse_answer} = undef;
+my $action = ParseTree($tree, $rules, $decision);
+triggerAction($action, (keys(%$cities))[0]) if(defined($action));
+# print STDERR Dump($decision) if(defined($verbose));
+
 # show cities.
 foreach my $cityId (keys(%$cities)) {
     printf("%s http://%s/index.php?view=city&id=%d\n", 
         $cities->{$cityId}->{name}, $::server, $cityId);
     printf("construction: %s\n", $cities->{$cityId}->{construction});
-    
-    foreach my $thing (qw/resources space force buildings citizens army fleet/) {
+
+    foreach my $thing (qw/resources space buildings citizens army fleet/) {
         printf("<%s>\n", uc($thing));
         foreach my $i (keys(%{$cities->{$cityId}->{$thing}})) {
             printf("%s %s, ", $i, $cities->{$cityId}->{$thing}->{$i});
@@ -170,35 +387,44 @@
         print("\n");
     }
     print("\n");
-    # print(Dumper($cities));
 
-    # make decisions
+    # maybe this should be moved to Rules.
+    $i->blanceHurmanResource($cityId);
+ 
+    # build spy
+    $i->buildSpy('spy', $cityId);
+    # enlarge the scientist number.
+    $i->set("academy", $cityId);
+    $i->set("tavern", $cityId);
 
-    # for the Decision Tree
+    # build military!
+    $tree  = LoadFile('military.yaml');
     $cities->{$cityId}->{parse_path} = [];
     $cities->{$cityId}->{parse_answer} = undef;
-    while (my $action = ParseTree($tree, $rules, $cities->{$cityId}))
-    {
-        # TODO: remove the last rule, if the result is fallback
+    if (my $action = ParseTree($tree, $rules, $cities->{$cityId})) {
         triggerAction($action, $cityId);
-        if($cities->{$cityId}->{parse_answer} ne "fallback") {
-            last ;
-        } else {
+    }
+    DumpFile("military-$cityId-dump.yaml", $cities->{$cityId});
 
-        }
+    # build and upgrade for cities
+    $tree  = LoadFile('city.yaml');
+    $cities->{$cityId}->{parse_path} = [];
+    $cities->{$cityId}->{parse_answer} = undef;
+    if (my $action = ParseTree($tree, $rules, $cities->{$cityId})) {
+        triggerAction($action, $cityId);
     }
-    # Debug
-    # print(Dumper($cities->{$cityId}->{parse_path}));
+    DumpFile("city-$cityId-dump.yaml", $cities->{$cityId});
 }
+DumpFile("research-dump.yaml", $i->{'research'});
+DumpFile("wars-dump.yaml", $i->{'military'});
 
 $i->logout;
 
-
 sub triggerAction {
     my ($action, $cityId) = @_;
 
     my ($func, $param) = split(/_/,$action);
-    printf('$i->%s("%s", %s);'. "\n\n", $func, $param, $cityId);
+    printf('$i->%s("%s", %s);'. "\n", $func, $param, $cityId);
     eval(sprintf('$i->%s("%s", %s);', $func, $param, $cityId));
     warn $@ if $@;
 }
--- a/ally.pl	Thu Oct 23 00:47:56 2008 +0800
+++ b/ally.pl	Mon Dec 01 15:32:44 2008 +0800
@@ -59,16 +59,16 @@
     }
     </style></head><body><table border=1>");
 
-foreach my $x ($x1..$x2)
+foreach my $y($y1..$y2)
 {
     print(OUT "<tr>");
-    foreach my $y($y1..$y2)
+    foreach my $x ($x1..$x2)
     {
         # printf("<div stlye='float:left; background-color: black; padding: 0; Display:inline;'>o</div>");
         if(defined($maps{$x}{$y}{'density'})) {
             my $c = 255 - (15 * $maps{$x}{$y}{'density'});
-            printf(OUT "<td style=\"background-color: rgb(255,%d,%d);\"><a href=\"http://s2.ikariam.tw/index.php?view=island&id=%d\" title=\"[%d,%d] (%d)\">[%d,%d]</a></td>",
-                $c, $c, $maps{$x}{$y}{'id'}, $x, $y, $maps{$x}{$y}{'density'}, $x, $y);
+            printf(OUT "<td style=\"background-color: rgb(255,%d,%d);\"><a href=\"http://%s/index.php?view=island&id=%d\" title=\"[%d,%d] (%d)\">[%d,%d]</a></td>",
+                $c, $c, $::server, $maps{$x}{$y}{'id'}, $x, $y, $maps{$x}{$y}{'density'}, $x, $y);
         } else {
             printf(OUT "<td style=\"background-color: rgb(255,255,255);\">[%d,%d]</td>", $x, $y);
         }
--- a/building.yaml	Thu Oct 23 00:47:56 2008 +0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,107 +0,0 @@
----
-# 基本建設規則
-# 檢查是否被攻擊
-- is_attacked:
-    # we are in Peace :D
-    0:
-      - is_constructing:
-         # already building something.
-         1:
-           # adjust human resources
-           # 
-           # 相關基礎研究 (technologies tree)
-           # 財富
-            - is_wealth_researched:
-               0: research_economy
-            # 擴張
-            - is_expansion_researched:
-               0: research_seafaring
-            # 造紙
-            - is_paper_researched:
-               0: research_knowledge
-            # 正規軍
-            - is_professionalarmy_researched:
-               0: resaerch_military
-            # 釀酒
-            - is_winepress_researched:
-               0: research_economy
-            # 發明
-            - is_invention_researched:
-               0: research_knowledge
-         # let's find something to build up
-         0:
-            - is_gold_enoughforcargo:
-               1: increaseTransporter
-            - is_wall_enough:
-               0: build_wall
-            - is_space_enough:
-               0: 
-                  - is_resource_enoghforHall:
-                     1: build_townHall
-            - is_corruption:
-               1: build_governorsresidence
-            # 倉庫庫存量
-            - is_warehouse_enough:
-               0: build_warehouse
-            - is_risk:
-               1: 
-                  - is_shipyard:
-                     0: 
-                        - is_drydock_researched: 
-                           0: resaerch_military
-                           1: build_shipyard
-                           # build one board
-            - is_happiness:
-               0: 
-                  - is_winepress_researched:
-                     # 0: research_economy
-                     1: build_tavern
-
-            # TODO
-            # http://ikariam.wikia.com/wiki/List_of_buildings
-            # http://ikariam.wikia.com/wiki/Technology_Tree
-            # is_conservation_researched
-            #    -build_warehouse
-            # build_academy
-            # build_palace (Expansion, Capital only)
-            # build_embassy (副城, 不建)
-
-            - is_professionalarmy_researched:
-               1:
-                  - is_shipyard_level_enough:
-                     1:
-                        - build_shipyard
-            - is_professionalarmy_researched:
-               1:
-                  - is_barracks_level_enough:
-                     1: 
-                        - build_barracks
-            # is_invention_researched
-            # biuld_workshop 
-            # biuld_hideout
-            #
-            # build_museum
-
-            # 皇宮
-            # 副城, 不建 ...
-    # 採取防禦措施
-    1: run_defense
-    # 若軍分 0, 進行焦土政策 針對特定城市 錢全部買船 拆港
-    # increaseTransporter
-    # destroy TradingPort
-    #
-    # 計算出兵數量、到達時間與調動時間
-    # 調動軍隊防護 (加入封鎖列表)
-
-    # (保留兵力) 出兵對方城鎮, 拆港, keep gold
-    # (保留兵力) 出兵任一城鎮, 拆港, keep gold
-
-# blocking
-# 計算軍方比例
-# 對方軍分 0, # 海軍一日三次, 每次八小時 # 陸軍每日三次, 八小時一次
-# 對方平等軍分, 半夜偷襲
-# 對方高軍分 # 累積分數
-#
-# balance resources
-# 從其他城移動資源到特定城市
-# 預先計算可能需要的資源
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/city.yaml	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,76 @@
+# TODO adjust human resources
+---
+# city level
+# 基本建設規則
+# 若遭受攻擊則暫停建設
+- is_attacked:
+    # we are in Peace :D
+    0:
+      - is_constructing:
+         0:
+            # FIXME: 這樣會一口氣買同等城市數量的船耶, 應拉出至全國金錢考量
+            # - is_gold_enoughforcargo:
+               # 1: increaseTransporter
+            - is_corruption:
+               1:
+                  # FIXME: 應該先升級皇宮
+                  - is_warehouse_enougn_for_governorsresidence:
+                     0: build_warehouse
+                     1: build_palaceColony
+            - is_any_corruption:
+               0: 
+                  - is_space_enough:
+                     0: 
+                        - is_resource_enoghforHall:
+                           1: build_townHall
+                  - is_happiness:
+                     0:
+                       - is_museum_enough:
+                          0:
+                           - is_culturalexchange_resaerched:
+                              1: build_museum
+                       - is_winepress_researched:
+                           1:
+                              - is_tavern_available:
+                                 0: build_tavern
+                                 1: 
+                                    - is_bacchanal:
+                                       0: set_tavern
+                                       1: build_tavern
+                  - is_wall_enough:
+                     0: build_wall
+                  - is_shipyard_level_enough:
+                     0:
+                        - is_professionalarmy_researched:
+                           1: build_shipyard
+                  - is_barracks_level_enough:
+                     0:
+                        - is_professionalarmy_researched:
+                           1: build_barracks
+                  - is_safehouse_enough:
+                     0:
+                        - is_espionage_researched:
+                           1: build_safehouse
+                  - is_academy_enough:
+                     0: build_academy
+                  # 大使館
+                  - is_embassy_enough:
+                     0:
+                        - is_foreigncultures_researched:
+                           1: build_embassy 
+                  - is_museum_enough:
+                     0:
+                        - is_culturalexchange_resaerched:
+                           1: build_museum
+                  - is_warehouse_enough:
+                     0:
+                        - is_conservation_researched:
+                           1: build_warehouse
+                           0: build_academy
+                  - is_branchOffice_enough:
+                     0: build_branchOffice
+
+# 水晶島應該建設 
+# is_invention_researched
+#    1: 
+#       - build_workshop 
--- a/enemy.pl	Thu Oct 23 00:47:56 2008 +0800
+++ b/enemy.pl	Mon Dec 01 15:32:44 2008 +0800
@@ -32,12 +32,13 @@
         }
         printf("Level: %s\n", $city->citylevel);
         printf("Island: %s\n", $island->name);
-        printf("Location: [%s:%s] http://s2.ikariam.tw/index.php?view=island&id=%s&selectCity=%d\n", 
-            $island->x, $island->y, $island->id, $city->cityId);
+        printf("Location: [%s:%s] http://%s/index.php?view=island&id=%s&selectCity=%d\n", 
+            $island->x, $island->y, $::server, $island->id, $city->cityId);
         printf("Tradegood: %s\n", $tradegoodText[$island->tradegood]);
         printf("Wonder: %s\n", $wonderText[$island->wonder]);
         printf("capture score: %d\n\n", $capture);
         # print Dumper($island->_data_hash);
         # print Dumper($city->_data_hash);
     }
+    print "-"x80 . "\n";
 }
--- a/freeland.pl	Thu Oct 23 00:47:56 2008 +0800
+++ b/freeland.pl	Mon Dec 01 15:32:44 2008 +0800
@@ -4,11 +4,13 @@
 use Data::Dumper;
 
 package main;
+my @tradegoodText = qw/NULL 葡萄酒 大理石 水晶 硫磺/;
+my @wonderText = qw/NULL 赫菲斯拓斯的熔爐 蓋亞的神殿 狄奧尼索斯的花園 雅典娜的神殿 赫秘士的神殿 阿瑞斯的要塞 波賽頓的神殿 克羅瑟斯的神殿/;
 
-if($#ARGV != 1) {
-    die("Usage: $0 x y\n");
+if($#ARGV != 2) {
+    die("Usage: $0 x y tradegood (1 葡萄酒, 2 大理石, 3 水晶, 4 硫磺/)\n");
 }
-my ($x, $y) = @ARGV;
+my ($x, $y, $tradegood) = @ARGV;
 
 my @location = (($x + 6), ($x - 6), ($y + 6), ($y - 6));
 
@@ -23,8 +25,9 @@
     }
 );
 
+# 
 my @islands = Ikariam::Island->retrieve_from_sql(qq{
-        tradegood == 2
+        tradegood == $tradegood
     AND people < 16
     AND x <= $location[0]
     AND x >= $location[1]
@@ -37,19 +40,25 @@
         foreach my $city (@cities) {
             my $island = Ikariam::Island->retrieve($city->island);
             my $user = Ikariam::User->retrieve($city->user);
-            printf("%s (%d) \"%s\" %d [%d,%d] http://s2.ikariam.tw/index.php?view=island&id=%d&selectCity=%d\n",
+            printf("%s (%d) \"%s\" %d [%d,%d] http://%s/index.php?view=island&id=%d&selectCity=%d\n",
                 $user->name, $user->score,
                 $city->cityname, $city->citylevel, 
                 $island->x, $island->y,
+                $::server,
                 $island->id,
                 $city->cityId
             );
         }
 } else {
+    # 依照距離列表
     foreach my $island (@islands)
     {
-        printf("[%d:%d] http://s2.ikariam.tw/index.php?view=island&id=%d%s\n",
+        printf("%d [%d:%d] %s %s http://%s/index.php?view=island&id=%d%s\n",
+            (abs($x-$island->x) + abs($y-$island->y)), # minutes ?
             $island->x, $island->y,
+            $tradegoodText[$island->tradegood],
+            $wonderText[$island->wonder],
+            $::server,
             $island->id,
         );
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hunting.yaml	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,34 @@
+---
+# 基本建設規則
+# 若糟攻擊則不建設
+- is_attacked:
+   0:
+# 計算當下戰爭次數
+# 本城之戰爭數
+# 維護費 / gold 比例
+
+# blocking (打到死政策)
+# 計算軍方比例
+# 對方軍分 0, 海軍一日三次, 每次八小時, 陸軍每日三次, 八小時一次
+# 對方平等軍分, 半夜偷襲
+# 對方高軍分, 累積分數後反攻
+
+# hunting
+# 分數低於我 0.1
+# 軍分低於我 0.3
+# 搶奪數量
+# 出戰比例運算
+#   間諜
+#   軍分成績
+
+   1: run_defense
+    # 遭受攻擊 採取防禦措施
+    # 若軍分 0, 進行焦土政策 針對特定城市 錢全部買船 拆港
+    # increaseTransporter
+    # destroy TradingPort
+    #
+    # 計算出兵數量、到達時間與調動時間
+    # 調動軍隊防護 (加入封鎖列表)
+
+    # (保留兵力) 出兵對方城鎮, 拆港, keep gold
+    # (保留兵力) 出兵任一城鎮, 拆港, keep gold
--- a/ikariam.sql	Thu Oct 23 00:47:56 2008 +0800
+++ b/ikariam.sql	Mon Dec 01 15:32:44 2008 +0800
@@ -1,17 +1,5 @@
-CREATE TABLE cities (
- cityId INTEGER PRIMARY KEY UNIQUE,
- citylevel INTEGER,
- cityname TEXT,
- user INTEGER,
- owner TEXT,
- ally TEXT,
- island INTEGER, "status" CHAR, "time" INTEGER);
-CREATE TABLE "island" (
- id BIGINT PRIMARY KEY UNIQUE,
- x INTEGER,
- y INTEGER,
- name TEXT,
- tradegood INTEGER,
- wonder INTEGER,
- people INTEGER, "time" INTEGER);
+CREATE TABLE "ally" ("id" INTEGER PRIMARY KEY  NOT NULL ,"name" TEXT,"members" INTEGER,"url" TEXT,"time" DATETIME,"score" BIGINT);
+CREATE TABLE cities ( cityId INTEGER PRIMARY KEY UNIQUE, citylevel INTEGER, cityname TEXT, user INTEGER, owner TEXT, ally TEXT, island INTEGER, "status" CHAR, "time" INTEGER, "allyId" INTEGER);
+CREATE TABLE "island" ( id BIGINT PRIMARY KEY UNIQUE, x INTEGER, y INTEGER, name TEXT, tradegood INTEGER, wonder INTEGER, people INTEGER, "time" INTEGER);
+CREATE TABLE "report" ("id" INTEGER PRIMARY KEY  NOT NULL , "cityLevel" INTEGER, "wallLevel" INTEGER, "attacker" TEXT, "defender" TEXT, "winner" TEXT, "targetCity" TEXT, "island" INTEGER NOT NULL , "city" INTEGER NOT NULL , "gold" INTEGER DEFAULT 0, "wood" INTEGER DEFAULT 0, "crystal" INTEGER DEFAULT 0, "wine" INTEGER DEFAULT 0, "sulfur" INTEGER DEFAULT 0, "date" DATETIME, "time" DATETIME);
 CREATE TABLE "user" ("id" INTEGER PRIMARY KEY ,"name" VARCHAR,"ally" VARCHAR,"allyId" INTEGER,"score" INTEGER,"building_score_main" INTEGER,"building_score_secondary" INTEGER,"research_score_main" INTEGER,"research_score_secondary" INTEGER,"army_score_main" INTEGER,"trader_score_secondary" INTEGER, "time" INTEGER);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/README	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,57 @@
+Definition
+
+    * GAE: Google App Engine
+    * GAEO: Google App Engine Oil ( Web Framwork )
+
+Requirment Of Develope Env
+
+    * download eagle-eye and setup it. see more in eagel-eye README
+    * download GAE SDK and setup it.
+    * download GAEO Framwork and setup it. see more in GAEO document.
+
+    * add following in ~/.bashrc
+
+        export GAEO_HOME='~/codes/gaeo'  # put your installation directory here.
+        export GAE_HOME='~/codes/google_appengine'
+        PATH=$PATH:$GAEO_HOME/bin:$GAE_HOME:~/bin/ 
+
+Launch Web site
+
+    * using GAE dev_server.py to launch.
+    * browse http://localhost:8080 to see the index page.
+
+Develope new version of models if chichchun has changed database schema.
+
+    if db schema has changed, the GAEO models in web site need to rebuild,
+    a model generator will get colum informations in all tables in ikariam.sqlite, 
+    and makes new GAEO models, which has a required properties, you may want to 
+    modify it later if some properties is wrong.
+
+    the model relation will not generate in this action, you have added it by hand.
+
+    [ WARRING ]
+    it will re-generate all models, the web site can not work until
+    you have done the work that re-design models.
+
+    $ ./rebuild_world
+    
+How many model are implemented already?
+ 	
+ 	player, island, city, ally
+    
+How to kwnow What input fileds of the model in POST ?
+ 
+ 	use action "attrs", example: http://localhost:8080/player/attrs
+
+How to display model information ?
+
+	use action "show", example: http://localhost:8080/player/show/1
+
+	p.s the last number 1 is the Ikariam Game ID of the model.
+
+Hot to get the datas of the model in JSON formate?
+
+	use action "json", example: http//"localhost:8080/player/json/1"
+	
+	p.s the last number 1 is the Ikariam Game ID of the model.
+ 	
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/app.yaml	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,17 @@
+application: ikweb
+version: 1
+api_version: 1
+runtime: python
+
+handlers:
+- url: /css
+  static_dir: assets/css
+- url: /js
+  static_dir: assets/js
+- url: /img
+  static_dir: assets/img
+- url: /favicon.ico
+  static_files: favicon.ico
+  upload: favicon.ico
+- url: .*
+  script: main.py
Binary file ikweb/ikweb/application/controller/__init__.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/application/controller/ally.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,9 @@
+from google.appengine.ext import db
+
+from gaeo.controller import BaseController
+
+from model.ally import Ally
+
+class AllyController(BaseController):
+
+    pass
\ No newline at end of file
Binary file ikweb/ikweb/application/controller/ally.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/application/controller/city.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,9 @@
+from google.appengine.ext import db
+
+from gaeo.controller import BaseController
+
+from model.city import City
+
+class CityController(BaseController):
+
+    pass
\ No newline at end of file
Binary file ikweb/ikweb/application/controller/city.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/application/controller/island.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,9 @@
+from google.appengine.ext import db
+
+from gaeo.controller import BaseController
+
+from model.island import Island
+
+class IslandController(BaseController):
+
+    pass
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/application/controller/player.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,10 @@
+from google.appengine.ext import db
+
+from gaeo.controller import BaseController
+
+from model.player import Player
+from model.ally import Ally
+
+class PlayerController(BaseController):
+    
+    pass
\ No newline at end of file
Binary file ikweb/ikweb/application/controller/player.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/application/controller/welcome.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,5 @@
+from gaeo.controller import BaseController
+
+class WelcomeController(BaseController):
+    def index(self):
+        pass
Binary file ikweb/ikweb/application/controller/welcome.pyc has changed
Binary file ikweb/ikweb/application/model/__init__.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/application/model/ally.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,12 @@
+from google.appengine.ext import db
+from gaeo.model import BaseModel
+
+class Ally(BaseModel):
+    game_id = db.IntegerProperty()
+    members_total = db.IntegerProperty(verbose_name='members_total',name='members_total')
+    name = db.TextProperty(verbose_name='name',name='name')
+    score = db.IntegerProperty(verbose_name='score',name='score')
+    lastupdate_at = db.DateTimeProperty(auto_now=True)
+    url = db.TextProperty(verbose_name='url',name='url')   
+
+    int_attrs = ['score','members_total','game_id']
\ No newline at end of file
Binary file ikweb/ikweb/application/model/ally.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/application/model/city.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,22 @@
+from google.appengine.ext import db
+from gaeo.model import BaseModel
+
+from model.island import Island
+from model.player import Player
+
+# city belongs to one player and one island.
+
+class City(BaseModel):
+    game_id = db.IntegerProperty()
+    name = db.TextProperty(verbose_name='name',name='name')
+    level = db.IntegerProperty(verbose_name='level',name='level')
+    island = db.ReferenceProperty(Island)
+    island_id = db.IntegerProperty(verbose_name='island_id',name='island_id')    
+    player = db.ReferenceProperty(Player)
+    player_id = db.IntegerProperty(verbose_name='player',name='player')    
+    status = db.StringProperty(verbose_name='status',name='status')
+    lastupdate_at = db.DateTimeProperty()
+    
+    unshow_attrs = ['owner_id','island_id']
+    ref_attrs = ['island_id','player_id']  
+    int_attrs = ['level','game_id']
Binary file ikweb/ikweb/application/model/city.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/application/model/island.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,10 @@
+from google.appengine.ext import db
+from gaeo.model import BaseModel
+
+class Island(BaseModel):
+    game_id = db.IntegerProperty()
+    name = db.TextProperty(verbose_name='name',name='name')
+    x = db.IntegerProperty(verbose_name='x',name='x')
+    y = db.IntegerProperty(verbose_name='y',name='y')
+
+    int_attrs = ['x','y']
\ No newline at end of file
Binary file ikweb/ikweb/application/model/island.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/application/model/player.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,26 @@
+from google.appengine.ext import db
+from gaeo.model import BaseModel
+
+from model.ally import Ally
+
+class Player(BaseModel):
+    game_id = db.IntegerProperty()
+    ally = db.ReferenceProperty(Ally)
+    ally_id = db.IntegerProperty(verbose_name='ally_id',name='ally_id')    
+    army_score_main = db.IntegerProperty(verbose_name='army_score_main',name='army_score_main')
+    building_score_main = db.IntegerProperty(verbose_name='building_score_main',name='building_score_main')
+    building_score_secondary = db.IntegerProperty(verbose_name='building_score_secondary',name='building_score_secondary')
+    name = db.StringProperty(verbose_name='name',name='name')
+    research_score_main = db.IntegerProperty(verbose_name='research_score_main',name='research_score_main')
+    research_score_secondary = db.IntegerProperty(verbose_name='research_score_secondary',name='research_score_secondary')
+    score = db.IntegerProperty(verbose_name='score',name='score')
+    trader_score_secondary = db.IntegerProperty(verbose_name='trader_score_secondary',name='trader_score_secondary')
+    lastupdate_at = db.DateTimeProperty(auto_now=True)
+    
+    unshow_attrs = ['ally']
+    
+    int_attrs = ['army_score_main', 'building_score_main',
+                 'building_score_secondary', 'research_score_main',
+                 'research_score_secondary', 'score',
+                 'trader_score_secondary',
+                'game_id', 'ally_id']
\ No newline at end of file
Binary file ikweb/ikweb/application/model/player.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/application/model/report.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,20 @@
+from google.appengine.ext import db
+from gaeo.model import BaseModel
+
+class Report(BaseModel):
+    attacker = db.TextProperty(verbose_name='attacker',name='attacker')
+    city = db.IntegerProperty(verbose_name='city',name='city')
+    cityLevel = db.IntegerProperty(verbose_name='cityLevel',name='cityLevel')
+    crystal = db.IntegerProperty(default=None,verbose_name='crystal',name='crystal')
+    date = db.DatetimeProperty(verbose_name='date',name='date')
+    defender = db.TextProperty(verbose_name='defender',name='defender')
+    gold = db.IntegerProperty(default=None,verbose_name='gold',name='gold')
+    id = db.IntegerProperty(verbose_name='id',name='id')
+    island = db.IntegerProperty(verbose_name='island',name='island')
+    sulfur = db.IntegerProperty(default=None,verbose_name='sulfur',name='sulfur')
+    targetCity = db.TextProperty(verbose_name='targetCity',name='targetCity')
+    time = db.DatetimeProperty(verbose_name='time',name='time')
+    wallLevel = db.IntegerProperty(verbose_name='wallLevel',name='wallLevel')
+    wine = db.IntegerProperty(default=None,verbose_name='wine',name='wine')
+    winner = db.TextProperty(verbose_name='winner',name='winner')
+    wood = db.IntegerProperty(default=None,verbose_name='wood',name='wood')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/gaeo/__init__.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2008 Lin-Chieh Shangkuan & Liang-Heng Chen
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+""" The gaeo library package. """
+
+import re
+
+from google.appengine.ext import webapp
+
+from gaeo.dispatch import dispatcher
+
+
+class Config:
+    """ The singleton of GAEO's configuration """
+
+    class __impl:
+        def __init__(self):
+            self.template_dir = ''
+            self.session_store = 'memcache'
+            self.app_name = ''
+
+    __instance = None
+
+    def __init__(self):
+        if Config.__instance is None:
+            Config.__instance = Config.__impl()
+
+        self.__dict__['_Config__instance'] = Config.__instance
+
+    def __getattr__(self, attr):
+        return getattr(self.__instance, attr)
+
+    def __setattr__(self, attr, value):
+        return setattr(self.__instance, attr, value)
+
+
+class MainHandler(webapp.RequestHandler):
+    """Handles all requests
+    """
+    def get(self, *args):
+        self.__process_request()
+
+    def post(self, *args):
+        self.__process_request()
+        
+    def head(self, *args):
+        self.__process_request()
+        
+    def options(self, *args):
+        self.__process_request()
+        
+    def put(self, *args):
+        self.__process_request()
+        
+    def delete(self, *args):
+        self.__process_request()
+        
+    def trace(self, *args):
+        self.__process_request()
+
+    def __process_request(self):
+        """dispatch the request"""
+        dispatcher.dispatch(self)
+
Binary file ikweb/ikweb/gaeo/__init__.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/gaeo/controller/__init__.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,237 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2008 GAEO Team.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""GAEO controller package
+"""
+
+import new
+import os
+import re
+import logging
+
+from google.appengine.ext.webapp import template
+
+import gaeo
+import errors
+import helper
+
+class BaseController(object):
+    """The BaseController is the base class of action controllers.
+        Action controller handles the requests from clients.
+    """
+    def __init__(self, hnd, params = {}):
+        self.hnd = hnd
+        self.resp = self.response = hnd.response
+        self.req = self.request = hnd.request
+        self._env = self.request.environ # shortcut
+        self.params = params
+
+        rp = hnd.request.params.mixed()
+        for k in rp:
+            self.params[k] = rp[k]
+
+        self._controller = params['controller']
+        self._action = params['action']
+        self.has_rendered = False
+        self.__config = gaeo.Config()
+
+        self.__tpldir = os.path.join(
+            self.__config.template_dir,
+            self._controller
+        )
+        self._template_values = {}
+
+        # implement parameter nesting as in rails
+        self.params=self.__nested_params(self.params)
+        
+        # detect the mobile platform
+        self._is_mobile = self.__detect_mobile()
+        self._is_iphone = self.__detect_iphone()
+
+        # alias the cookies
+        self.cookies = self.request.cookies
+
+        # create the session
+        try:
+            store = self.__config.session_store
+            exec('from gaeo.session.%s import %sSession' %
+                (store, store.capitalize()))
+        
+            self.session = eval('%sSession' % store.capitalize())(
+                                hnd, '%s_session' % self.__config.app_name)
+        except:
+            raise errors.ControllerInitError('Initialize Session Error!')
+
+        # add request method (get, post, head, put, ....)
+        self._request_method = self.request.environ['REQUEST_METHOD'].lower()
+        
+        # tell if an ajax call (X-Request)
+        self._is_xhr = self._env.has_key('HTTP_X_REQUESTED_WITH') and \
+                        self._env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
+        
+
+        # add helpers
+        helpers = dir(helper)
+        for h in helpers:
+            if not re.match('^__', h):
+                self.__dict__[h] = new.instancemethod(eval('helper.%s' % h), self, BaseController)
+
+    #this can be used in application controller, it is meant to be called before every controller
+    def implicit_action(self):
+        pass 
+
+    def before_action(self):
+        pass
+    
+    def after_action(self):
+        pass
+        
+    def invalid_action(self):
+        """ If the router went to an invalid action """
+        self.hnd.error(404)
+        self.render(text="Invalid action")
+
+    def to_json(self, obj, **kwds):
+        """ Convert a dict to JSON. Inspired from SimpleJSON """
+        from gaeo.controller.jsonencoder import JSONEncoder
+        
+        if not kwds:
+            return JSONEncoder(skipkeys=False, ensure_ascii=True, check_circular=True,
+                allow_nan=True, indent=None, separators=None, encoding='utf-8',
+                default=None).encode(obj)
+        else:
+            return JSONEncoder(
+                skipkeys=False, ensure_ascii=True, check_circular=True,
+                allow_nan=True, indent=None, separators=None,
+                encoding='utf-8', default=None, **kwds).encode(obj)
+
+    def render(self, *html, **opt):
+        o = self.resp.out
+        h = self.resp.headers
+
+        # check the default Content-Type is 'text/html; charset=utf-8'
+        h['Content-Type'] = 'text/html; charset=utf-8'
+        if html:
+            for h in html:
+                o.write(h.decode('utf-8'))
+        elif opt:
+            if opt.get('html'):
+                o.write(opt.get('html').decode('utf-8'))                
+            elif opt.get('text'):
+                h['Content-Type'] = 'text/plain; charset=utf-8'
+                o.write(str(opt.get('text')).decode('utf-8'))
+            elif opt.get('json'):
+                h['Content-Type'] = 'application/json; charset=utf-8'
+                o.write(opt.get('json').decode('utf-8'))
+            elif opt.get('xml'):
+                h['Content-Type'] = 'text/xml; charset=utf-8'
+                o.write(opt.get('xml').decode('utf-8'))
+            elif opt.get('script'):
+                h['Content-Type'] = 'text/javascript; charset=utf-8'
+                o.write(opt.get('script').decode('utf-8'))
+            elif opt.get('template'):
+                context = self.__dict__
+                if isinstance(opt.get('values'), dict):
+                    context.update(opt.get('values'))
+                o.write(template.render(
+                    os.path.join(self.__tpldir,
+                                 opt.get('template') + '.html'),
+                    context
+                ))
+            elif opt.get('template_string'):
+                context = self.__dict__
+                if isinstance(opt.get('values'), dict):
+                    context.update(opt.get('values'))
+                from django.template import Context, Template
+                t = Template(opt.get('template_string').encode('utf-8'))
+                c = Context(context)
+                o.write(t.render(c))
+            elif opt.get('image'): # for sending an image content
+                import imghdr
+                img_type = imghdr.what('ignored_filename', opt.get('image'))
+                h['Content-Type'] = 'image/' + img_type
+                o.write(opt.get('image'))
+            else:
+                raise errors.ControllerRenderTypeError('Render type error')
+        self.has_rendered = True
+
+    def redirect(self, url, perm = False):
+        self.has_rendered = True # dirty hack, make gaeo don't find the template
+        self.hnd.redirect(url, perm)
+
+    def respond_to(self, **blk):
+        """ according to self.params['format'] to respond appropriate stuff
+        """
+        if self.params.has_key('format') and blk.has_key(self.params['format']):
+            logging.error(self.params['format'])
+            blk[self.params['format']]()
+
+    def __detect_mobile(self):
+        h = self.request.headers
+
+        # wap.wml
+        ha = h.get('Accept')
+        if ha and (ha.find('text/vnd.wap.wml') > -1 or ha.find('application/vnd.wap.xhtml+xml') > -1):
+            return True
+        
+        wap_profile = h.get('X-Wap-Profile')
+        profile = h.get("Profile")
+        opera_mini = h.get('X-OperaMini-Features')
+        ua_pixels = h.get('UA-pixels')
+        
+        if wap_profile or profile or opera_mini or ua_pixels:
+            return True
+        
+        # FIXME: add common user agents
+        common_uas = ['sony', 'noki', 'java', 'midp', 'benq', 'wap-', 'wapi']
+        
+        ua = h.get('User-Agent')
+        if ua and ua[0:4].lower() in common_uas:
+            return True
+        
+        return False
+        
+    def __detect_iphone(self):
+        """ for detecting iPhone/iPod """
+        ua = self.request.headers.get('User-Agent')
+        if ua:
+            ua = ua.lower();
+            return ua.find('iphone') > -1 or ua.find('ipod') > -1
+        else:
+            return False
+            
+
+    # Helper methods for parameter nesting as in rails
+    def __appender(self,dict,arr,val):
+            if len(arr) > 1:
+                try:
+                    dict[arr[0]]
+                except KeyError:
+                    dict[arr[0]]={}
+                return {arr[0]: self.__appender(dict[arr[0]],arr[1:],val)}
+            else:
+                dict[arr[0]]=val
+                return 
+
+    def __nested_params(self,prm):
+        prm2={}
+        for param in prm:
+            parray = param.replace(']',"").split('[')
+            if len(parray) == 1:
+                parray = parray[0].split('-')
+            self.__appender(prm2,parray,prm[param])
+        return prm2
+        
Binary file ikweb/ikweb/gaeo/controller/__init__.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/gaeo/controller/errors.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2008 Lin-Chieh Shangkuan & Liang-Heng Chen
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+""" The gaeo controller errors """
+
+class ControllerError(Exception):
+    """ Base error class of controllers' errors """
+
+class ControllerInitError(ControllerError):
+    pass
+
+class ControllerRenderError(ControllerError):
+    """ error occured while render """
+
+class ControllerRenderTypeError(ControllerRenderError):
+    """ Render an invalid type """
Binary file ikweb/ikweb/gaeo/controller/errors.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/gaeo/controller/helper.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2008 Lin-Chieh Shangkuan & Liang-Heng Chen
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+""" helper module """
+
+def clear_session(self):
+    if self.session:
+        import logging
+        logging.info('clear session')
+        self.session.invalidate()
\ No newline at end of file
Binary file ikweb/ikweb/gaeo/controller/helper.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/gaeo/controller/jsonencoder.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,406 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#Copyright (c) 2006 Bob Ippolito
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy of
+# this software and associated documentation files (the "Software"), to deal in
+# the Software without restriction, including without limitation the rights to
+# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+# of the Software, and to permit persons to whom the Software is furnished to do
+# so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+"""
+Implementation of JSONEncoder
+"""
+import re
+
+try:
+    from simplejson._speedups import encode_basestring_ascii as c_encode_basestring_ascii
+except ImportError:
+    pass
+
+ESCAPE = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t]')
+ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])')
+HAS_UTF8 = re.compile(r'[\x80-\xff]')
+ESCAPE_DCT = {
+    '\\': '\\\\',
+    '"': '\\"',
+    '\b': '\\b',
+    '\f': '\\f',
+    '\n': '\\n',
+    '\r': '\\r',
+    '\t': '\\t',
+}
+for i in range(0x20):
+    ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,))
+
+# Assume this produces an infinity on all machines (probably not guaranteed)
+INFINITY = float('1e66666')
+FLOAT_REPR = repr
+
+def floatstr(o, allow_nan=True):
+    # Check for specials.  Note that this type of test is processor- and/or
+    # platform-specific, so do tests which don't depend on the internals.
+
+    if o != o:
+        text = 'NaN'
+    elif o == INFINITY:
+        text = 'Infinity'
+    elif o == -INFINITY:
+        text = '-Infinity'
+    else:
+        return FLOAT_REPR(o)
+
+    if not allow_nan:
+        raise ValueError("Out of range float values are not JSON compliant: %r"
+            % (o,))
+
+    return text
+
+
+def encode_basestring(s):
+    """
+    Return a JSON representation of a Python string
+    """
+    def replace(match):
+        return ESCAPE_DCT[match.group(0)]
+    return '"' + ESCAPE.sub(replace, s) + '"'
+
+
+def py_encode_basestring_ascii(s):
+    if isinstance(s, str) and HAS_UTF8.search(s) is not None:
+        s = s.decode('utf-8')
+    def replace(match):
+        s = match.group(0)
+        try:
+            return ESCAPE_DCT[s]
+        except KeyError:
+            n = ord(s)
+            if n < 0x10000:
+                return '\\u%04x' % (n,)
+            else:
+                # surrogate pair
+                n -= 0x10000
+                s1 = 0xd800 | ((n >> 10) & 0x3ff)
+                s2 = 0xdc00 | (n & 0x3ff)
+                return '\\u%04x\\u%04x' % (s1, s2)
+    return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"'
+
+
+try:
+    encode_basestring_ascii = c_encode_basestring_ascii
+except NameError:
+    encode_basestring_ascii = py_encode_basestring_ascii
+
+
+class JSONEncoder(object):
+    """
+    Extensible JSON <http://json.org> encoder for Python data structures.
+
+    Supports the following objects and types by default:
+    
+    +-------------------+---------------+
+    | Python            | JSON          |
+    +===================+===============+
+    | dict              | object        |
+    +-------------------+---------------+
+    | list, tuple       | array         |
+    +-------------------+---------------+
+    | str, unicode      | string        |
+    +-------------------+---------------+
+    | int, long, float  | number        |
+    +-------------------+---------------+
+    | True              | true          |
+    +-------------------+---------------+
+    | False             | false         |
+    +-------------------+---------------+
+    | None              | null          |
+    +-------------------+---------------+
+
+    To extend this to recognize other objects, subclass and implement a
+    ``.default()`` method with another method that returns a serializable
+    object for ``o`` if possible, otherwise it should call the superclass
+    implementation (to raise ``TypeError``).
+    """
+    __all__ = ['__init__', 'default', 'encode', 'iterencode']
+    item_separator = ', '
+    key_separator = ': '
+    def __init__(self, skipkeys=False, ensure_ascii=True,
+            check_circular=True, allow_nan=True, sort_keys=False,
+            indent=None, separators=None, encoding='utf-8', default=None):
+        """
+        Constructor for JSONEncoder, with sensible defaults.
+
+        If skipkeys is False, then it is a TypeError to attempt
+        encoding of keys that are not str, int, long, float or None.  If
+        skipkeys is True, such items are simply skipped.
+
+        If ensure_ascii is True, the output is guaranteed to be str
+        objects with all incoming unicode characters escaped.  If
+        ensure_ascii is false, the output will be unicode object.
+
+        If check_circular is True, then lists, dicts, and custom encoded
+        objects will be checked for circular references during encoding to
+        prevent an infinite recursion (which would cause an OverflowError).
+        Otherwise, no such check takes place.
+
+        If allow_nan is True, then NaN, Infinity, and -Infinity will be
+        encoded as such.  This behavior is not JSON specification compliant,
+        but is consistent with most JavaScript based encoders and decoders.
+        Otherwise, it will be a ValueError to encode such floats.
+
+        If sort_keys is True, then the output of dictionaries will be
+        sorted by key; this is useful for regression tests to ensure
+        that JSON serializations can be compared on a day-to-day basis.
+
+        If indent is a non-negative integer, then JSON array
+        elements and object members will be pretty-printed with that
+        indent level.  An indent level of 0 will only insert newlines.
+        None is the most compact representation.
+
+        If specified, separators should be a (item_separator, key_separator)
+        tuple.  The default is (', ', ': ').  To get the most compact JSON
+        representation you should specify (',', ':') to eliminate whitespace.
+
+        If specified, default is a function that gets called for objects
+        that can't otherwise be serialized.  It should return a JSON encodable
+        version of the object or raise a ``TypeError``.
+
+        If encoding is not None, then all input strings will be
+        transformed into unicode using that encoding prior to JSON-encoding.
+        The default is UTF-8.
+        """
+
+        self.skipkeys = skipkeys
+        self.ensure_ascii = ensure_ascii
+        self.check_circular = check_circular
+        self.allow_nan = allow_nan
+        self.sort_keys = sort_keys
+        self.indent = indent
+        self.current_indent_level = 0
+        if separators is not None:
+            self.item_separator, self.key_separator = separators
+        if default is not None:
+            self.default = default
+        self.encoding = encoding
+
+    def _newline_indent(self):
+        return '\n' + (' ' * (self.indent * self.current_indent_level))
+
+    def _iterencode_list(self, lst, markers=None):
+        if not lst:
+            yield '[]'
+            return
+        if markers is not None:
+            markerid = id(lst)
+            if markerid in markers:
+                raise ValueError("Circular reference detected")
+            markers[markerid] = lst
+        yield '['
+        if self.indent is not None:
+            self.current_indent_level += 1
+            newline_indent = self._newline_indent()
+            separator = self.item_separator + newline_indent
+            yield newline_indent
+        else:
+            newline_indent = None
+            separator = self.item_separator
+        first = True
+        for value in lst:
+            if first:
+                first = False
+            else:
+                yield separator
+            for chunk in self._iterencode(value, markers):
+                yield chunk
+        if newline_indent is not None:
+            self.current_indent_level -= 1
+            yield self._newline_indent()
+        yield ']'
+        if markers is not None:
+            del markers[markerid]
+
+    def _iterencode_dict(self, dct, markers=None):
+        if not dct:
+            yield '{}'
+            return
+        if markers is not None:
+            markerid = id(dct)
+            if markerid in markers:
+                raise ValueError("Circular reference detected")
+            markers[markerid] = dct
+        yield '{'
+        key_separator = self.key_separator
+        if self.indent is not None:
+            self.current_indent_level += 1
+            newline_indent = self._newline_indent()
+            item_separator = self.item_separator + newline_indent
+            yield newline_indent
+        else:
+            newline_indent = None
+            item_separator = self.item_separator
+        first = True
+        if self.ensure_ascii:
+            encoder = encode_basestring_ascii
+        else:
+            encoder = encode_basestring
+        allow_nan = self.allow_nan
+        if self.sort_keys:
+            keys = dct.keys()
+            keys.sort()
+            items = [(k, dct[k]) for k in keys]
+        else:
+            items = dct.iteritems()
+        _encoding = self.encoding
+        _do_decode = (_encoding is not None
+            and not (_encoding == 'utf-8'))
+        for key, value in items:
+            if isinstance(key, str):
+                if _do_decode:
+                    key = key.decode(_encoding)
+            elif isinstance(key, basestring):
+                pass
+            # JavaScript is weakly typed for these, so it makes sense to
+            # also allow them.  Many encoders seem to do something like this.
+            elif isinstance(key, float):
+                key = floatstr(key, allow_nan)
+            elif isinstance(key, (int, long)):
+                key = str(key)
+            elif key is True:
+                key = 'true'
+            elif key is False:
+                key = 'false'
+            elif key is None:
+                key = 'null'
+            elif self.skipkeys:
+                continue
+            else:
+                raise TypeError("key %r is not a string" % (key,))
+            if first:
+                first = False
+            else:
+                yield item_separator
+            yield encoder(key)
+            yield key_separator
+            for chunk in self._iterencode(value, markers):
+                yield chunk
+        if newline_indent is not None:
+            self.current_indent_level -= 1
+            yield self._newline_indent()
+        yield '}'
+        if markers is not None:
+            del markers[markerid]
+
+    def _iterencode(self, o, markers=None):
+        if isinstance(o, basestring):
+            if self.ensure_ascii:
+                encoder = encode_basestring_ascii
+            else:
+                encoder = encode_basestring
+            _encoding = self.encoding
+            if (_encoding is not None and isinstance(o, str)
+                    and not (_encoding == 'utf-8')):
+                o = o.decode(_encoding)
+            yield encoder(o)
+        elif o is None:
+            yield 'null'
+        elif o is True:
+            yield 'true'
+        elif o is False:
+            yield 'false'
+        elif isinstance(o, (int, long)):
+            yield str(o)
+        elif isinstance(o, float):
+            yield floatstr(o, self.allow_nan)
+        elif isinstance(o, (list, tuple)):
+            for chunk in self._iterencode_list(o, markers):
+                yield chunk
+        elif isinstance(o, dict):
+            for chunk in self._iterencode_dict(o, markers):
+                yield chunk
+        else:
+            if markers is not None:
+                markerid = id(o)
+                if markerid in markers:
+                    raise ValueError("Circular reference detected")
+                markers[markerid] = o
+            for chunk in self._iterencode_default(o, markers):
+                yield chunk
+            if markers is not None:
+                del markers[markerid]
+
+    def _iterencode_default(self, o, markers=None):
+        newobj = self.default(o)
+        return self._iterencode(newobj, markers)
+
+    def default(self, o):
+        """
+        Implement this method in a subclass such that it returns
+        a serializable object for ``o``, or calls the base implementation
+        (to raise a ``TypeError``).
+
+        For example, to support arbitrary iterators, you could
+        implement default like this::
+            
+            def default(self, o):
+                try:
+                    iterable = iter(o)
+                except TypeError:
+                    pass
+                else:
+                    return list(iterable)
+                return JSONEncoder.default(self, o)
+        """
+        raise TypeError("%r is not JSON serializable" % (o,))
+
+    def encode(self, o):
+        """
+        Return a JSON string representation of a Python data structure.
+
+        >>> JSONEncoder().encode({"foo": ["bar", "baz"]})
+        '{"foo": ["bar", "baz"]}'
+        """
+        # This is for extremely simple cases and benchmarks.
+        if isinstance(o, basestring):
+            if isinstance(o, str):
+                _encoding = self.encoding
+                if (_encoding is not None 
+                        and not (_encoding == 'utf-8')):
+                    o = o.decode(_encoding)
+            if self.ensure_ascii:
+                return encode_basestring_ascii(o)
+            else:
+                return encode_basestring(o)
+        # This doesn't pass the iterator directly to ''.join() because the
+        # exceptions aren't as detailed.  The list call should be roughly
+        # equivalent to the PySequence_Fast that ''.join() would do.
+        chunks = list(self.iterencode(o))
+        return ''.join(chunks)
+
+    def iterencode(self, o):
+        """
+        Encode the given object and yield each string
+        representation as available.
+        
+        For example::
+            
+            for chunk in JSONEncoder().iterencode(bigobject):
+                mysocket.write(chunk)
+        """
+        if self.check_circular:
+            markers = {}
+        else:
+            markers = None
+        return self._iterencode(o, markers)
+
+__all__ = ['JSONEncoder']
Binary file ikweb/ikweb/gaeo/controller/jsonencoder.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/gaeo/dispatch/__init__.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2008 Lin-Chieh Shangkuan & Liang-Heng Chen
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Dispatcher Package
+"""
\ No newline at end of file
Binary file ikweb/ikweb/gaeo/dispatch/__init__.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/gaeo/dispatch/dispatcher.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,132 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2008 Lin-Chieh Shangkuan & Liang-Heng Chen
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import re
+import logging
+
+import router
+import sys 
+import os
+from traceback import *
+
+HTTP_ERRORS = {
+    '400': 'Bad Request',
+    '402': 'Payment Required',
+    '403': 'Forbidden',
+    '404': 'Not Found',
+    '500': 'Internal Server Error'
+}
+
+TXMT_LINKS = False # set true to show textmate links on tracebacks
+DEBUG = True # set true to show traceback on error pages
+
+def dispatch(hnd):
+    
+    # generate nice traceback with optional textmate links
+    def nice_traceback(traceback):
+        tb=""
+        for line in traceback.splitlines(1):
+            filename = re.findall('File "(.+)",', line)
+            linenumber = re.findall(', line\s(\d+),', line)
+            modulename = re.findall(', in ([A-Za-z]+)', line)
+            if filename and linenumber and not re.match("<(.+)>",filename[0]):
+                fn=filename[0]
+                mn="in %s" % modulename[0] if modulename else ""
+                fnshort=os.path.basename(fn)
+                ln=linenumber[0]
+                if TXMT_LINKS:
+                    html="<a href='txmt://open/?url=file://%s&line=%s'>%s:%s %s</a> %s" % (fn,ln,fnshort,ln,mn,line)
+                else:
+                    html="<b>%s:%s %s</b> %s" % (fnshort,ln,mn,line)
+                tb+=html
+            else:
+                tb+=line
+        return tb
+    
+    # show error and write to log
+    def show_error(code, log_msg = ''):
+        hnd.error(code)
+        if sys.exc_info()[0]:
+            exception_name = sys.exc_info()[0].__name__
+            exception_details = str(sys.exc_info()[1])
+            exception_traceback = ''.join(format_exception(*sys.exc_info()))
+            special_info = str(exception_details) != str(log_msg)
+            logging.error(exception_name)
+            logging.error(exception_details)
+            logging.error(log_msg)
+            logging.error(exception_traceback)
+            hnd.response.out.write('<h1>%s</h1>' % HTTP_ERRORS[str(code)])
+            if DEBUG:
+                tb=nice_traceback(exception_traceback)
+                if special_info: logging.error(log_msg)
+                hnd.response.out.write('<h3>%s: %s</h3>' % (exception_name, exception_details))
+                if special_info: hnd.response.out.write('<pre> %s </pre>' % log_msg)
+                hnd.response.out.write('<h1> Traceback </h1>')
+                hnd.response.out.write('<pre> %s </pre>' % tb)
+        else:
+            hnd.response.out.write('<h1> %s </h1>' % log_msg)
+
+    # resolve the URL
+    url = hnd.request.path
+    r = router.Router()
+    route = r.resolve(url)
+
+    if route is None:
+        try:
+            raise Exception('invalid URL')
+        except Exception, e:
+            show_error(500, e)
+    else:
+        # create the appropriate controller
+        try:
+            exec('from controller import %s' % route['controller']) in globals()
+            ctrl = eval('%s.%sController' % (
+                        route['controller'],
+                        route['controller'].capitalize()
+                    ))(hnd, route)
+
+            # dispatch
+            logging.info('URL "%s" is dispatched to: %sController#%s',
+                         url,
+                         route['controller'].capitalize(),
+                         route['action'])
+        except ImportError, e:
+            show_error(404, "Controller doesn't exist")
+        except AttributeError, e:  # the controller has not been defined.
+            show_error(404, "Controller doesn't exist")
+        except Exception, e:
+            show_error(500, e)
+        else:
+            try:
+                ctrl.attrs
+                action = getattr(ctrl, route['action'], None)
+                if action is not None:
+                    ctrl.implicit_action()
+                    ctrl.before_action()
+                    action()
+                    ctrl.after_action()
+
+                    if not ctrl.has_rendered:
+                        ctrl.render(template=route['action'], values=ctrl.__dict__)
+                else: # invalid action
+                    logging.error('Invalid action `%s` in `%s`' % (route['action'], route['controller']))
+                    try:
+                        raise Exception('invalid action')
+                    except Exception, e:
+                        show_error(500, e)
+            except Exception, e:
+                show_error(500, e)
Binary file ikweb/ikweb/gaeo/dispatch/dispatcher.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/gaeo/dispatch/router.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2008 Lin-Chieh Shangkuan & Liang-Heng Chen
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import re
+from copy import copy
+import logging
+
+class RuleError(Exception):
+    """Base Error"""
+
+class RuleNoControllerError(RuleError):
+    """No controller"""
+
+class Rule(object):
+    """ Handles each routing rule. """
+    def __init__(self, pattern, **param):
+        super(Rule, self).__init__()
+
+        self.pattern = pattern[:-1] if pattern.endswith('/') else pattern
+        self.regex = self.pattern
+        self.param = param
+        self.matches = re.findall(':([^/\.]+)', self.pattern)
+
+        for i in range(len(self.matches)):
+            self.regex = self.regex.replace(':' + self.matches[i], '([^/\.]+)')
+            self.param[self.matches[i]] = i
+        self.validate()
+
+    def __eq__(self, other):
+        return self.regex == other.regex
+
+    def __getattr__(self, attr):
+        try:
+            return getattr(self, 'param')[attr]
+        except KeyError:
+            raise AttributeError, attr
+
+    def __str__(self):
+        from operator import itemgetter
+        return ', '.join(['%s: %s' % (k, v) for k, v in \
+            sorted(self.param.items(), key = itemgetter(1))])
+
+    def match_url(self, url):
+        if url.endswith('/'):
+            url = url[:-1]
+        try:
+            mat = re.findall(self.regex, url)[0]
+        except IndexError:
+            return None
+
+        param = copy(self.param)
+        if isinstance(mat, basestring):
+            if self.matches:
+                param[self.matches[0]] = mat
+        elif isinstance(mat, tuple):
+            for i in range(len(mat)):
+                param[self.matches[i]] = mat[i]
+        return param
+
+    def url_for(self, controller, **param):
+        param['controller'] = controller
+        url = self.pattern
+        for match in self.matches:
+            if match not in param:
+                return None
+            url = url.replace(':' + match, str(param[match]))
+            del param[match]
+
+        # extra parameters
+        ep = '&'.join(['%s=%s' % (k, v) for k, v in param.items() if k not in self.param])
+
+        return url + '?' + ep if ep else url
+
+    def validate(self):
+        if 'controller' not in self.param:
+            raise RuleNoControllerError
+
+        if 'action' not in self.param:
+            self.param['action'] = 'index'
+
+        if not self.regex.startswith('^'):
+            self.regex = '^' + self.regex
+        if not self.regex.endswith('$'):
+            self.regex = self.regex + '$'
+
+
+class Router:
+    """ Handles the url routing... """
+
+    class __impl:
+        def __init__(self):
+            self.__routing_root = {
+                'controller': 'welcome',
+                'action': 'index',
+            }
+            self.__routing_table = []
+            # used to store default pattern (but match last)
+            self.__routing_table_fallback = [
+                Rule('/:controller/:action'),
+                Rule('/:controller')
+            ]
+
+        def connect(self, pattern, **tbl):
+            """ Add routing pattern """
+
+            rule = Rule(pattern, **tbl)
+            if rule not in self.__routing_table:
+                self.__routing_table.append(rule)
+
+        def disconnect(self, pattern):
+            rule = Rule(pattern)
+            if rule in self.__routing_table:
+                self.__routing_table.remove(rule)
+
+        def root(self, **map):
+            """ Set the root (/) routing... """
+            self.__routing_root['controller'] = \
+                map.get('controller', self.__routing_root['controller'])
+            self.__routing_root['action'] = \
+                map.get('action', self.__routing_root['action'])
+
+        def resolve(self, url):
+            """ Resolve the url to the correct mapping """
+
+            if url == '/':
+                return self.__routing_root
+
+            ret = self.__resolve_by_table(url, self.__routing_table)
+            if ret is None: # fallback
+                ret = self.__resolve_by_table(url, self.__routing_table_fallback)
+            return ret
+
+        def __resolve_by_table(self, url, rules):
+            """ Resolve url by the given table """
+            for r in rules:
+                ret = r.match_url(url)
+                if ret:
+                    return ret
+            return None
+
+        def url_for(self, controller, **param):
+            for r in self.__routing_table:
+                ret = r.url_for(controller, **param)
+                if ret:
+                    return ret
+            return None
+
+    __instance = None
+
+    def __init__(self):
+        if Router.__instance is None:
+            Router.__instance = Router.__impl()
+        self.__dict__['_Router__instance'] = Router.__instance
+
+    def __getattr__(self, attr):
+        return getattr(self.__instance, attr)
+
+    def __setattr__(self, attr, value):
+        return setattr(self.__instance, attr, value)
+
Binary file ikweb/ikweb/gaeo/dispatch/router.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/gaeo/model/__init__.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,118 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2008 GAEO Team.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""GAEO model package
+"""
+import re
+
+from google.appengine.ext import db
+
+def pluralize(noun):
+    if re.search('[sxz]$', noun):
+        return re.sub('$', 'es', noun)
+    elif re.search('[^aeioudgkprt]h$', noun):
+        return re.sub('$', 'es', noun)
+    elif re.search('[^aeiou]y$', noun):
+        return re.sub('y$', 'ies', noun)
+    else:
+        return noun + 's'
+
+class BaseModel(db.Model):
+    """BaseModel is the base class of data model."""
+
+    @classmethod
+    def belongs_to(cls, ref_cls):
+        """ Declare a many-to-one relationship """
+        if ref_cls is None:
+            raise Exception('No referenced class')
+        
+        ref_name = ref_cls.__name__.lower()
+        if ref_name not in cls._properties:
+            attr = db.ReferenceProperty(ref_cls, collection_name=pluralize(cls.__name__.lower()))
+            cls._properties[ref_name] = attr
+            attr.__property_config__(cls, ref_name)
+
+    @classmethod
+    def has_and_belongs_to_many(cls, ref_cls):
+        if ref_cls is None:
+            raise Exception('No referenced class')
+        
+        f_name = pluralize(cls.__name__.lower())
+        t_name = pluralize(ref_cls.__name__.lower())
+        
+        if t_name not in cls._properties:
+            attr = db.ListProperty(db.Key)
+            cls._properties[t_name] = attr
+            attr.__property_config__(cls, t_name)
+        if f_name not in ref_cls._properties:
+            attr = property(lambda self: cls.gql('WHERE %s = :1' % t_name, self.key()))
+            ref_cls._properties[f_name] = attr
+            attr.__property_config__(ref_cls, f_name)
+    
+    @classmethod
+    def named_scope(cls, name, order_by=None, **conds):
+        if name not in cls._properties:
+            cond_str = "WHERE "
+            for cond in conds.iterkeys():
+                if len(cond_str) > 6:
+                    cond_str += ' AND '
+                cond_str += '%s %s' % (cond, conds[cond])
+                
+            if order_by:
+                cond_str += ' ORDER BY %s' % order_by
+                
+            attr = property(lambda self: cls.gql(cond_str))
+            cls._properties[name] = attr
+            attr.__property_config__(cls, name)
+    
+    def update_attributes(self, kwd_dict = {}, **kwds):
+        """Update the specified properties"""
+        need_change = False
+        
+        # if user passed a dict, merge to kwds (Issue #3)
+        if kwd_dict:
+            kwd_dict.update(kwds)
+            kwds = kwd_dict
+        
+        props = self.properties()
+        for prop in props.values():
+            if prop.name in kwds:
+                if not need_change:
+                    need_change = True
+                prop.__set__(self, kwds[prop.name])
+        
+        if need_change:
+            self.update()
+
+    def set_attributes(self, kwd_dict = {}, **kwds):
+        """set the specified properties, but not update"""
+        
+        # Issue #3
+        if kwd_dict:
+            kwd_dict.update(kwds)
+            kwds = kwd_dict
+        
+        props = self.properties()
+        for prop in props.values():
+            if prop.name in kwds:
+                prop.__set__(self, kwds[prop.name])
+        
+    def save(self):
+        self.put()
+        
+    def update(self):
+        self.put()
+        
Binary file ikweb/ikweb/gaeo/model/__init__.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/gaeo/session/__init__.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2008 Lin-Chieh Shangkuan & Liang-Heng Chen
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+""" GAEO Session package """
+import logging
+from random import choice
+from string import digits, letters
+
+SESSIONID_LEN = 64
+POOL = digits + letters
+
+class Session(dict):
+    """ Session is an abstract class that declares sessions basic
+    operations. """
+
+    def __init__(self, hnd, name, timeout):
+        """The Session's constructor.
+
+        @param hnd      The webapp.ReqeustHanlder object.
+        @param name     The session name.
+        @param timeout  The time interval (in sec) that the session expired.
+        """
+
+        dict.__init__(self)
+        self._name = name
+        self._hnd = hnd
+        self._timeout = timeout
+        self._id = ''.join([ choice(POOL) for i in range(SESSIONID_LEN) ])
+
+        self._invalidated = False
+
+    def save(self):
+        pass
+
+    def invalidate(self):
+        pass
Binary file ikweb/ikweb/gaeo/session/__init__.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/gaeo/session/memcache.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2008 Lin-Chieh Shangkuan & Liang-Heng Chen
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+""" GAEO Session - memcache store """
+import random
+import pickle
+import logging
+
+from google.appengine.api import memcache
+
+from gaeo import session
+
+class MemcacheSession(session.Session):
+    """ session that uses memcache """
+
+    def __init__(self, hnd, name = 'gaeo_session', timeout = 60 * 60):
+        super(MemcacheSession, self).__init__(hnd, name, timeout)
+
+        # check from cookie
+        if name in hnd.request.cookies:
+            self._id = hnd.request.cookies[name]
+            session_data = memcache.get(self._id)
+            if session_data:
+                self.update(pickle.loads(session_data))
+                memcache.set(self._id, session_data, timeout)
+        else:   # not in the cookie, set it
+            cookie = '%s=%s' % (name, self._id)
+            hnd.response.headers.add_header('Set-Cookie', cookie)
+
+    def put(self):
+        if not self._invalidated:
+            memcache.set(self._id, pickle.dumps(self.copy()), self._timeout)
+
+    def invalidate(self):
+        """Invalidates the session data"""
+        self._hnd.response.headers.add_header(
+            'Set-Cookie',
+            '%s=; expires=Thu, 1-Jan-1970 00:00:00 GMT;' % self._name
+        )
+        memcache.delete(self._id)
+        self.clear()
+        self._invalidated = True
Binary file ikweb/ikweb/gaeo/session/memcache.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/gaeo/view/__init__.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2008 GAEO Team.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+""" GAEO View package """
Binary file ikweb/ikweb/gaeo/view/__init__.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/gaeo/view/helper/__init__.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2008 GAEO Team
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""" GAEO view helper module """
\ No newline at end of file
Binary file ikweb/ikweb/gaeo/view/helper/__init__.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/gaeo/view/helper/ajax.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2008 GAEO Team
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""" GAEO view ajax helpers """
+
+def link_to_function(title, script, html = {}):
+    """ Return a link that contains onclick scripts """
+    html_part = ''
+    if html:
+        for key, val in html.items():
+            space = ' ' if html_part else ''
+            html_part = '%s%s="%s"' % (space, key, val)
+        
+    return '<a href="#" %s onclick="%s;return false;">%s</a>' % (html_part, script, title)
+
+
+def javascript_tag(scripts):
+    """ Wraps scripts with <script> tag. """
+    return '<script type="text/javascript">%s</script>' % scripts
+
+
+def remote_script(url, **opts):
+    """ Create a ajax request script """
+    method = opts.get('method', 'get');
+    # request parameters
+    data = opts.get('data', '')
+    # callback function
+    callback = opts.get('callback', '')    
+    dataType = opts.get('dataType', '')
+
+    params = "'%s'" % data if data else ''
+    if callback:
+        cbf = 'function(data, textStatus){%s}' % callback
+        params = params + ', %s' % cbf if params else cbf
+    params = params + ", '%s'" % dataType if dataType else params
+    if params:
+        params = ', ' + params
+
+    return """$.%s('%s'%s)""" % (method, url, params)
+
+def link_to_remote(title, url, **opts):
+    """ Create a link that request a remote action (jQuery-based) """
+    # check if the title is specified
+    if not title:
+        raise Exception('Missing title of the link.')
+    
+    # remote action
+    if not url:
+        raise Exception('Missing remote action.')
+    
+    script = remote_script(url, **opts)    
+    return link_to_function(title, script, opts.get('html', {}))
+
+
+def load_from_remote(title, url, target, **opts):
+    """ Create a link that load data from a remote action (jQuery-based) """
+    # check if the title is specified
+    if not title:
+        raise Exception('Missing title of the link.')
+
+    # remote action url
+    if not url:
+        raise Exception('Missing remote action.')
+        
+    # load target #id
+    if not target:
+        raise Exception('Missing the id of loaded data target.')
+        
+    data = opts.get('data', '')
+    callback = opts.get('callback', '')    
+    params = data
+    if callback:
+        cbf = 'function(){%s}' % callback
+        params = '%s,%s' % (params, cbf) if params else cbf
+    
+    params = ', ' + params if params else ''
+    script = """$('#%s').load('%s'%s);""" % (target, url, params)
+    
+    return link_to_function(title, script, opts.get('html', {}))
+
+
+def periodically_call_remote(url, **opts):
+    """ Periodically call a remote action. (jQuery-based) """
+    if not url:
+        raise Exception('Missing remote action url.')
+    
+    # frequency, default 1000 ms
+    freq = opts.get('frequency', 1000)
+    script = "setInterval(function(){%s}, %s)" % (remote_script(url, **opts), freq)
+    return javascript_tag(script)
+    
Binary file ikweb/ikweb/gaeo/view/helper/ajax.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/index.yaml	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,23 @@
+indexes:
+
+# AUTOGENERATED
+
+# This index.yaml is automatically updated whenever the dev_appserver
+# detects that a new type of query is run.  If you want to manage the
+# index.yaml file manually, remove the above marker line (the line
+# saying "# AUTOGENERATED").  If you want to manage some indexes
+# manually, move them above the marker line.  The index.yaml file is
+# automatically uploaded to the admin console when you next deploy
+# your application using appcfg.py.
+
+# Used once in query history.
+- kind: Player
+  properties:
+  - name: game_id
+  - name: lastupdate_at
+
+# Used 4 times in query history.
+- kind: Player
+  properties:
+  - name: lastupdate_at
+    direction: desc
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/main.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,56 @@
+import logging
+import os
+import re
+import sys
+import wsgiref.handlers
+
+from google.appengine.ext import webapp
+
+import gaeo
+from gaeo.dispatch import router
+
+def initRoutes():
+    r = router.Router()
+    
+    #TODO: add routes here
+
+    r.connect('/:controller/:action/:id')
+
+def initPlugins():
+    """
+    Initialize the installed plugins
+    """
+    plugins_root = os.path.join(os.path.dirname(__file__), 'plugins')
+    if os.path.exists(plugins_root):
+        plugins = os.listdir(plugins_root)
+        for plugin in plugins:
+            if not re.match('^__', plugin):
+                try:
+                    exec('from plugins import %s' % plugin)
+                except ImportError:
+                    logging.error('Unable to import %s' % (plugin))
+                except SyntaxError:
+                    logging.error('Unable to import name %s' % (plugin))
+
+def main():
+    # add the project's directory to the import path list.
+    sys.path.append(os.path.dirname(__file__))
+    sys.path.append(os.path.join(os.path.dirname(__file__), 'application'))
+
+    # get the gaeo's config (singleton)
+    config = gaeo.Config()
+    # setup the templates' location
+    config.template_dir = os.path.join(
+        os.path.dirname(__file__), 'application', 'templates')
+
+    initRoutes()
+    # initialize the installed plugins
+    initPlugins()
+
+    app = webapp.WSGIApplication([
+                (r'.*', gaeo.MainHandler),
+            ], debug=True)
+    wsgiref.handlers.CGIHandler().run(app)
+
+if __name__ == '__main__':
+    main()
Binary file ikweb/ikweb/plugins/__init__.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/plugins/ikariam/__init__.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,105 @@
+def load_model(modelname):
+    try:
+        return eval(modelname)
+    except NameError:
+        module = __import__("model.%s" % modelname.lower(), {}, {}, [modelname])
+        return getattr(module,modelname)
+        
+class IkariamModelPlugin:
+    
+    server = "s4"
+    base_url = "http://%s.ikariam.tw" % server
+    
+    @classmethod
+    def get_by_gameid(cls, id):
+        obj = cls.gql("WHERE game_id ="+str(id)+" ORDER BY lastupdate_at").get()
+        return obj
+    
+    def _mk_attrs(self):
+        from copy import copy
+        attrs = copy(self.params)
+        
+        for ref_name in ref_attrs:
+            try:
+                modelname = self.params[ref_name].replace('_id','')
+                model = load_model(modelname)
+                obj = model.get_by_game_id(self.params[ref_name])            
+                attrs[modelname] = obj
+            except KeyError:
+                pass
+        
+        return self._tr_attrtype(attrs, 'int', int_attrs)    
+
+class IkaraimControllerPlugin:
+
+    def _get_modelname(self):
+        return self.__class__.__name__.replace('Controller','')
+
+    def attrs(self):                       
+        modelname = self._get_modelname()
+        model = load_model(modelname)
+        
+        def fn(e):
+            if e not in getattr(model, 'unshow_attrs', []) and \
+               e not in ['__module__','__doc__','_properties',
+                         'unshow_attrs', 'int_attrs', 'lastupdate_at']:
+                return e
+        
+        self.ui().render_result(200, '<br/>'.join(filter(fn, model.__dict__.keys())))
+
+    def show(self):
+        datas = self._get_modeldatas()
+        
+        content = []        
+        for k,v in datas.items():
+            content.append("%s:%s<br/>"%(k,v))
+        
+        self.ui().render_result(200, ''.join(content))
+    
+    def json(self):
+        datas = self._get_modeldatas()
+        self.ui().render_json_of(datas)
+  
+    def _get_modeldatas(self):
+        modelname = self._get_modelname()
+        model = load_model(modelname)
+        obj = model.get_by_gameid(self.params['id'])  
+        return self._tr_attrtype(obj._entity,'str')
+    
+    def create(self):
+        self._update('create')
+
+    def edit(self):
+        self._update('edit')
+        
+    def _update(self, mode='create'):
+        modelname = self._get_modelname()
+        model = load_model(modelname)
+        
+        if 'edit' == mode:
+            obj = model.get_by_gameid(self.params['game_id'])
+        else:
+            obj = model
+        attrs = self._mk_attrs()
+        obj.update_attributes(attrs)
+        obj.save()  
+        
+    def _tr_attrtype(self, attrs, type, include_attrs=None):
+        
+        if not include_attrs:
+            attrnames = attrs.keys()
+        else:
+            attrnames = include_attrs    
+        
+        for attrname in attrnames:
+            try:
+                attrs[attrname] = eval(type)(attrs[attrname])
+            except KeyError:
+                pass
+        return attrs        
+
+from gaeo.model import BaseModel
+BaseModel.__bases__ += (IkariamModelPlugin,)
+
+from gaeo.controller import BaseController
+BaseController.__bases__ += (IkaraimControllerPlugin,)
\ No newline at end of file
Binary file ikweb/ikweb/plugins/ikariam/__init__.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/ikweb/plugins/lazyui/__init__.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,132 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2008 Hsin Yi, Chen
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License
+from gaeo.view.helper import ajax
+
+class LazyUiPlugin:
+    
+    def ui(self, frontend = 'html'):
+        """
+        get web page ui helper.
+        # close dialog window.
+        self.ui().close_dialog();
+        """ 
+        cls = 'Lazy'+frontend.capitalize()+'Ui'
+        self.__dict__.setdefault('_uihelper', eval(cls)(self) )
+        return self.__dict__.get('_uihelper')
+             
+class LazyHtmlUi:
+    __instance = None
+    
+    def __init__(self, ctrl):
+    	"""
+    	Controller Decoration.
+    	"""
+        if LazyHtmlUi.__instance is None:
+            LazyHtmlUi.__instance = ctrl
+    
+    def __getattr__(self, attr):
+        return getattr(self.__instance, attr)
+
+    def __setattr__(self, attr, value):
+        return setattr(self.__instance, attr, value)    
+    
+    def close_dialog(self, **after_act):
+        """
+        close web borwser diaolog window.
+        
+        # close the window and reload parent page.
+        close_dialog(after_act = {'action':'update_parent'}
+        
+        # using custom javascript code to controll parent page, for excample
+        # remove a block.
+        callback_js = self.create_controlls_parent_js('Editor.inserHTML', 'hello')
+        close_dialog(after_act = {'msg':'sucess.','action':'update_parent', 'callback':callback_js})
+                
+        # close the window and redirect.
+        close_dialog(after_act = {'action':'redirect','to_url':'http://www.example.com'})        
+        """
+        scripts = after_act.get('msg') and "alert('%s');" % after_act.get('msg')
+       
+        if 'redirect' == after_act.get('action'):
+            self.redirect(after_act.get('to_url', '/') )
+        elif 'update_parent' == after_act.get('action'):
+            if after_act.get('callback'):
+                scripts += after_act.get('callback')
+            else:
+            	scripts += "window.parent.location.reload();"
+          	        
+        scripts += "window.close();"
+        self.render_js(scripts)
+
+    def controlls_parent(self, ctrl, *argvs ):
+        """
+        calls parent window function, or object method.
+        
+        # insert a JPG image with 500x500 size to parent window.
+        controlls_parent_window('ImgEditor.insert', [ '/images/no.jpg', 500, 500] )
+        """
+        scripts = self.create_controlls_parent_window_js(ctrl, argvs)
+        self.render_js(scripts)
+        
+    def create_controlls_parent_js(self, ctrl, *argvs):
+    	return "window.parent.%s(%s);" % ( ctrl, str(argvs) and ','.join( [ '"'+str(x)+'"' for x in argvs ] ) )
+        
+    def alert_and_redirect(self, msg = '', dest_url = None):
+        """
+        alert message and redirect to next url if destention url is speficied,
+        otherwise, the web siet viewer will be redirect back to his last browsed page.        
+        
+        alert_and_redirect('action success!', 'http://example.com')
+        """
+        scripts = "alert('%s');" % msg
+        if dest_url:
+            scripts += "document.location.href='%s'" % ( msg, dest_url )
+        else:
+            scripts += "window.back();"
+        self.render_js(scripts)    
+    
+    def render_result(self, http_code=200, content=''):
+        """
+        display result info for frontend controller.
+		for example, swfupload file uploader requires a page that 
+		displays http state code to know the result after uploading
+     	files is finished.
+        """
+        self.hnd.error(http_code)
+        self.render(content)
+        
+    def render_js(self, scripts):
+        """
+        render javasciprt code to output.
+        
+        render_js("alert('hellow');document.location.href='/'")
+        """
+        self.render(ajax.javascript_tag(scripts))
+        
+    def render_json_of(self, obj):
+        """
+        render JSON of an object
+        """
+        self.render(self.to_json(obj))        
+
+    def render_xml_of(self, obj):
+        """
+        render XML of an object
+        """
+        self.render(self.to_xml(obj))
+
+from gaeo.controller import BaseController
+BaseController.__bases__ += (LazyUiPlugin,) 
Binary file ikweb/ikweb/plugins/lazyui/__init__.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/rebuild_world	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,8 @@
+#/bin/bash
+EAGLE_EYE=$HOME/lazyproject/eagle-eye
+echo "setup ikariam web site."
+
+db_schema=$EAGLE_EYE/ikariam.sqlite
+echo "load sqlite file:"$db_schema
+cd ikweb
+python ../tools/lazygen_model.py sqlite:///$db_schema
Binary file ikweb/tools/dummy.sqlite has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/tools/functional_test.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,29 @@
+from lazy.www import c
+
+def create_player():
+    datas = {'game_id':1,
+             'army_score_main':10000,
+             'building_score_main':1000,
+             'building_score_secondary':100,
+             'name':'tplayer',
+             'research_score_main':122,
+             'research_score_secondary':122,
+             'score':10000,
+             'trader_score_secondary':1000000    
+    }
+    c("http://localhost:8080/player/create").post(datas)
+    
+def edit_player():
+    datas = {'game_id':1,
+             'name':'tfplayer'
+             }
+    c("http://localhost:8080/player/edit").post(datas)
+    
+import sys
+
+act = sys.argv[1]
+modelname = sys.argv[2]
+
+funcname = "%s_%s" % (act,modelname)
+print 'test',funcname
+eval(funcname)()
Binary file ikweb/tools/lazy/__init__.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/tools/lazy/www/README.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,16 @@
+"""
+Requirements:
+
+	lxml - python libxml binding
+
+	it needs to installing the following packages before install lxml.
+
+    *  libxml 2.6.21 or later. It can be found here: http://xmlsoft.org/downloads.html
+    *  libxslt 1.1.15 or later. It can be found here: http://xmlsoft.org/XSLT/downloads.html
+		
+	If you use Ubuntu, here is what you need to do.
+	
+	$ apt-get install libxml2-dev libxslt1-dev
+	$ eazy_install lxml
+	
+"""
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/tools/lazy/www/__init__.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2008 Hsin Yi, Chen
+"""
+    [Note] the project is not available yet.
+
+    A web page fetcing tool chain that has a JQuery-like selector and supports chain working.
+    
+    Here is an exmaple can show the the main idea, To restrive a content you want
+    in a div box in a web page, and then post and restrive next content in the other
+    web page with the param you just maked from the content in first restriving.
+    finally, storage the production.
+    
+    def func(s):
+        return {'msg':s}
+    
+    try:
+        c("http://example.tw/").get().find("////ul/text()") \
+            .build_param( func ).post_to("http://example2.com") \
+            .save_as('hellow.html')
+    except:
+        pass
+        
+    more complex example
+        
+    try:
+        c("http://example.tw/").retry(4, '5m').get() \
+            .find("#id > div"). \
+            .build_param( func ).post_to("http://example2.com") \
+            .save_as('hellow.html') \
+            .end().find("#id2 > img").download('pretty-%s.jpg'). \
+            tar_and_zip("pretty_girl.tar.gz")
+    except NotFound:
+        print "the web page is not found."
+    except NoPermissionTosave:
+        print "the files can not be save with incorrect permission."
+    else:
+        print "unknow error."
+"""
+from lazy.www.work import WorkFlow
+from lazy.www.work.fetch import Fetcher
+from lazy.www.work.storage import FileStorager
+from lazy.www.core import SemiProduct
+import os
+import sys
+import re
+
+def parse_scheme(scheme):
+    try:
+        return re.findall("(\w+):\/\/(.*\/?)",scheme)[0]
+    except:
+        sys.stdout.write("the scheme is not supported.")
+        sys.exit()
+
+def c(scheme, worker=None):
+    """
+    connect to a web apge
+    
+    >>> c('http://localhost:8080').get().worker.working_product.content
+    'It works!!\\n'
+    
+    >>> c('http://localhost:8080').get().find('//text()')
+    'It works!!\\n'    
+    """
+    target_type, path = parse_scheme(scheme)
+
+    #@todo: SemiProduct Factory.
+    if worker is None:
+        if 'file' == target_type:
+            s= SemiProduct(source=path)        
+            worker = FileStorager(s)
+        else:
+            s= SemiProduct(source=scheme)        
+            worker = Fetcher(s)
+
+    return WorkFlow(worker)
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
Binary file ikweb/tools/lazy/www/__init__.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/tools/lazy/www/core/__init__.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,12 @@
+                          
+class SemiProduct:
+
+    last_work = None
+    source = None
+    content = None
+    
+    def __init__(self, **kwds):
+        self.source = kwds.get('source','')        
+        
+    def __str__(self):        
+        return self.content
\ No newline at end of file
Binary file ikweb/tools/lazy/www/core/__init__.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/tools/lazy/www/core/utils.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,4 @@
+
+def mix_in(py_class, mixin_class):
+    if mixin_class not in py_class.__bases__:
+        py_class.__bases__ += (mixin_class,)
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/tools/lazy/www/work/__init__.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,11 @@
+from lazy.www.work.fetch import Fetcher
+from lazy.www.work.find import Finder
+from lazy.www.core import SemiProduct
+
+class WorkFlow:
+
+    def __init__(self, worker):
+        self.worker = worker
+        
+    def __getattr__(self, name):
+        return getattr(self.worker, name)
Binary file ikweb/tools/lazy/www/work/__init__.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/tools/lazy/www/work/fetch.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,72 @@
+import urllib2
+import urllib
+import cookielib
+import os
+
+class Fetcher:
+    
+    opener = None
+    
+    working_product = None
+    
+    """
+    A Semi Production Decoration for content fetching.
+    
+    handles content restriving.
+    
+    >>> o = Fetcher( SemiProduct(source="http://localhost:8080") )
+    >>> o.get().working_product.content
+    'It works!!\\n'
+    """
+    def __init__(self, working_product):
+        self.working_product = working_product
+    
+    def get(self, data = {}):
+        """        
+        send datas via http get method.
+        """        
+        res = urllib2.urlopen(self.working_product.source, urllib.urlencode(data))
+        return res.read()
+    
+    def post(self, data = {} ):
+        """
+        send datas via http post method.
+        
+        >>> o = Fetcher( SemiProduct(source="http://localhost:8080") )
+        >>> o.post({'a':'b'}).working_product.content
+        'It works!!\\n'
+        """        
+        res = urllib2.urlopen(self.working_product.source, urllib.urlencode(data))
+        return res.read()    
+
+    def refer(self, refer_url):
+        """
+        refer getter/setter.
+
+        >>> o = Fetcher( SemiProduct(source="http://localhost:8080") )
+        >>> o.refer('http://www.example.com')        
+        """
+        raise NotImplementedError
+
+    def retry(self, count = 0, intval = 0, timeout = 0):
+        """
+        retry to fetch the content.
+
+        >>> o = Fetcher( SemiProduct(source="http://localhost:8080") )
+        >>> o.retry(4)        
+        """        
+        raise NotImplementedError
+    
+class Retry:
+    
+    """
+     A Fetcher Decoration for retry goal.
+     
+     
+    """
+    def __init__(self, fetcher):
+        raise NotImplementedError
+    
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
\ No newline at end of file
Binary file ikweb/tools/lazy/www/work/fetch.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/tools/lazy/www/work/find.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,27 @@
+from lxml import etree
+from cStringIO import StringIO
+
+class Finder:
+
+    dom_tree = None
+    xpath = None
+
+    def __init__(self, working_product):
+        self.working_prodcut = working_product
+
+        self.encoding = 'utf8'
+    
+    def find(self, express , callback = None):
+        
+        if self.dom_tree is None:   self.set_dom_tree(self.working_prodcut.content)
+ 
+        xpath = self.dom_tree.xpath(express)
+        
+        if callback:    return self.callback(xpath)
+        return xpath
+
+    def set_dom_tree(self, content):
+        stream = StringIO(content)
+
+        parser = etree.XMLParser(encoding=self.encoding)
+        self.dom_tree = etree.parse(stream, parser)
\ No newline at end of file
Binary file ikweb/tools/lazy/www/work/find.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/tools/lazy/www/work/storage.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,34 @@
+class FileStorager:
+    
+    opener = None    
+    working_product = None
+    
+    """
+    A Semi Production Decoration for content storaging.
+    
+    handles content storaging.
+    
+    >>> o = Fetcher( SemiProduct(source="file:///tmp/a.txt") )
+    >>> o.get().working_product.content
+    'It works!!\\n'
+    """
+    def __init__(self, working_product):
+        self.working_product = working_product
+    
+    def get(self, data = {}):
+        """        
+        send datas via http get method.
+        """        
+        res = open(self.working_product.source)
+        return res.read()
+    
+    def post(self, data = {} ):
+        """
+        send datas via http post method.
+        
+        >>> o = Fetcher( SemiProduct(source="file:///tmp/a.txt") )
+        >>> o.post({'a':'b'}).working_product.content
+        'It works!!\\n'
+        """        
+        res = open(self.working_product.source)
+        return res.read()
\ No newline at end of file
Binary file ikweb/tools/lazy/www/work/storage.pyc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ikweb/tools/lazygen_model.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,228 @@
+#!/usr/bin/env python
+import re
+import os
+import sys
+
+from sqlalchemy import *
+# dirty import XD
+gaeo_home = os.getenv('GAEO_HOME').replace('~',os.getenv('HOME'))+'/bin'
+try:
+    sys.path.append(gaeo_home)
+    from gaeogen import *
+except ImportError, e:
+    print "can not import gaeogen, the gaeogen path maybe wrong, got path:%s" % gaeo_home
+    sys.exit()
+
+
+def sqlite_column_mapper(model_class, table_obj):
+    """
+    map sqlite columns of the table to GAEO Model
+    
+    >>> metadata = MetaData()
+    >>> users_table = Table('users', metadata,
+    ...     Column('id', Integer, primary_key=True),
+    ...     Column('name', String),
+    ...     Column('fullname', String),
+    ...     Column('password', String)
+    ... )
+    >>> model = sqlite_column_mapper(GenModel, users_table)
+    >>> model.generate_content()
+    """
+    def convertor(col_type):
+        lowertype = col_type.lower()
+
+        if re.match('char', lowertype): col_type = "String"
+        if re.match('(int|integer)', lowertype):    col_type = 'Integer'
+        return col_type
+    
+    model = model_class(table_obj.name)
+    
+    for column in table_obj.columns:        
+        model.add_property( "%s" % GenProperty(column.name, 
+                                               column, 
+                                               convertor))
+    return model
+
+class GenProperty(object):
+    """
+    GAE Property Generator.
+        
+    class Property(verbose_name=None, name=None, default=None, required=False, validator=None, choices=None)
+    """
+    db_prefix = "^(SL|PG|Max|MS|FB|Ac|Info|Oracle)"        
+
+    def __init__(self, name, column=False, convertor=False):
+        """
+        >>> col = Column('summary', String(2000))
+        >>> p = GenProperty('summary', col)
+        >>> "%s" % p
+        "summary:StringProperty(verbose_name='summary',name='summary')"
+        >>> meta = MetaData()
+        >>> meta.bind = create_engine("sqlite:///dummy.sqlite")
+        >>> t = Table('ally', meta, autoload=True)        
+        >>> p = GenProperty('name', t.columns.get('name'))
+        >>> "%s" % p
+        "name:TextProperty(verbose_name='name',name='name')"
+        >>> p = GenProperty('name', t.columns.get('time'))
+        >>> "%s" % p
+        "name:DatetimeProperty(verbose_name='name',name='name')"
+        """
+        self.name = name
+        self.column = column
+        
+        # very dirty ...
+        if column:
+                self.type_name = re.sub( self.db_prefix,'',
+                                         column.type.__class__.__name__)
+                if convertor:
+                    self.type_name = convertor(self.type_name)
+        
+    def __str__(self):
+        return self.gen()
+    
+    def gen(self):
+        """
+        generate gaeo property code.
+        """
+        clause= self.create_clause(self.type_name, self.column) 
+        return gae_property_code(self.name, self.type_name, clause)        
+    
+    def create_clause(self, type, column):
+        """
+        create property cluase.
+                
+        >>> s = Column('summary', String(2000))
+        >>> code = GenProperty(s.name).create_clause('string', s)
+        >>> code
+        {'verbose_name': 'summary', 'name': 'summary'}
+        """
+        clause= self.create_basic_clause(column)
+        
+        try:
+            other_caluse = getattr(self, 'create_%s_clause' % type.lower())(column)
+            clause.update(other_caluse)            
+        except:
+            pass
+        
+        return clause
+    
+    def create_basic_clause(self, column):
+        """
+        create basic property cluase.
+        """
+        clause = {'verbose_name':self.name, 'name':self.name}
+            
+        if column.default:
+            clause.setdefault('default', column.default)
+        elif column.server_default:
+            clause.setdefault('default', column.server_default)
+            
+        return clause
+
+def gae_property_code(name, type, clause):
+    """
+    generate google app engine property code.
+    
+    >>> gae_property_code('name', 'string', {'default':'hychen','name':'hi'})
+    "name:StringProperty(default='hychen',name='hi')"
+    """
+    argv = []
+    
+    def _quote(v):
+        try:
+            return "'"+v+"'"
+        except TypeError:
+            pass
+    
+    for k,v in clause.items():
+        argv.append("%s=%s" % (k, _quote(v) ) )
+
+    return "%s:%sProperty(%s)" % (name, 
+                                  type.capitalize(),
+                                   ','.join(argv))
+
+class ModelMaker:
+
+    is_build = False
+    
+    def __init__(self, **kwds):
+        """
+        to create GAEO Model
+        
+        >>> maker = ModelMaker(schema="sqlite:///dummy.sqlite", autoload=True)
+        >>> maker.build()        
+        """
+        self.models = []
+        self.set_engine(kwds['schema'])
+        
+        try:                        
+            if True == kwds['autoload']:
+                self.table_names = self.db_engine.table_names()
+            else:
+                self.table_names = kwds['include_tables']
+        except KeyError:
+            print "input wrong argv."
+        
+    def set_engine(self, schema):
+        """
+        set db engine
+        """
+        self.db_engine = create_engine(schema)        
+        self.meta = MetaData()        
+        self.meta.bind = self.db_engine
+
+    def build(self):
+        """
+        build models by talbse in database.
+        """
+        mapper_func = '%s_column_mapper' % self.db_engine.name.lower()
+        
+        for table_name in self.table_names:
+            table = Table(table_name, self.meta, autoload=True)            
+            self.models.append( eval(mapper_func)( GenModel, table) )
+            
+        self.is_build = True
+            
+    def save(self):
+        """
+        save model
+        """
+        if self.is_build:
+            application_dir = os.path.join(os.getcwd(), 'application')
+            model_dir = os.path.join(application_dir, 'model')
+                        
+            # check if the model directory had been created
+            if not os.path.exists(os.path.join(model_dir, '__init__.py')):
+                create_file(os.path.join(model_dir, '__init__.py'), [])
+                                        
+            for model in self.models:
+                print 'Creating Model %s ...' % model.name
+                model.save(os.path.join(model_dir, '%s.py' % model.name))
+        else:
+            print "not build yet."
+
+def gen_models_from_db(db_schema, include_tables=False):
+    """
+    generate models form database.
+    
+    gen_models_from_db("sqlite://dumy.sqlite")
+    
+    # spefiy tables
+    gen_models_from_db("sqlite://dumy.sqlite", include_tables=['tb1','tb2'])
+    """
+    if not include_tables:
+        maker = ModelMaker(schema=db_schema, autoload=True)
+    else:
+        maker = ModelMaker(schema=db_schema, include_tables=include_tables)
+        
+    maker.build()
+    maker.save()
+
+if '__main__' == __name__:
+    import sys
+    
+    if 'test' == sys.argv[1]:    
+        import doctest
+        doctest.testmod()
+    else:
+        gen_models_from_db(sys.argv[1])
--- a/inactive-map.pl	Thu Oct 23 00:47:56 2008 +0800
+++ b/inactive-map.pl	Mon Dec 01 15:32:44 2008 +0800
@@ -10,13 +10,13 @@
 Ikariam::User->set_sql(inactivity => qq {
     SELECT user.id 
       FROM user, cities, island
-      WHERE user.id == cities.user
-        AND cities.island == island.id
+      WHERE user.id = cities.user
+        AND cities.island = island.id
         AND island.x <= ?
         AND island.x >= ?
         AND island.y <= ?
         AND island.y >= ?
-        AND cities.status == 'i'
+        AND cities.status = 'i'
     }
 );
 #Ikariam::User->set_sql(sheeps => qq {
@@ -36,9 +36,9 @@
 Ikariam::User->set_sql(sheeps => qq {
         SELECT user.id 
           FROM user, cities 
-         WHERE user.id == cities.user 
-           AND cities.status == 'i'
-           AND user.trader_score_secondary >= 50000 
+         WHERE user.id = cities.user 
+           AND cities.status = 'i'
+           AND user.trader_score_secondary >= 50000
            AND user.army_score_main <= 40
     }
 );
@@ -69,7 +69,7 @@
             # 所得金錢 = 對方城鎮等級x(對方城鎮等級-1)x對方金錢/10000
             my $robbery = $c->citylevel * ($c->citylevel - 1) * $sheep->trader_score_secondary / 10000;
 
-            next if($robbery < 2000);
+            next if($robbery < 1000);
 
             $maps{$island->x}{$island->y}{'id'} = $island->id;
             $maps{$island->x}{$island->y}{'density'} += 1;
@@ -95,16 +95,16 @@
         }
         </style></head><body><table border=1>");
 
-    foreach my $x ($x1..$x2)
+    foreach my $y($y1..$y2)
     {
         print(OUT "<tr>");
-        foreach my $y($y1..$y2)
+        foreach my $x ($x1..$x2)
         {
             # printf("<div stlye='float:left; background-color: black; padding: 0; Display:inline;'>o</div>");
             if(defined($maps{$x}{$y}{'density'})) {
                 my $c = 255 - (15 * $maps{$x}{$y}{'density'});
-                printf(OUT "<td style=\"background-color: rgb(255,%d,%d);\"><a href=\"http://s2.ikariam.tw/index.php?view=island&id=%d\" title=\"[%d,%d] (%d)\">[%d,%d]</a></td>",
-                    $c, $c, $maps{$x}{$y}{'id'}, $x, $y, $maps{$x}{$y}{'density'}, $x, $y);
+                printf(OUT "<td style=\"background-color: rgb(255,%d,%d);\"><a href=\"http://%s/index.php?view=island&id=%d\" title=\"[%d,%d] (%d)\">[%d,%d]</a></td>",
+                    $c, $c, $::server, $maps{$x}{$y}{'id'}, $x, $y, $maps{$x}{$y}{'density'}, $x, $y);
             } else {
                 printf(OUT "<td style=\"background-color: rgb(255,255,255);\">[%d,%d]</td>", $x, $y);
             }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/military.yaml	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,62 @@
+---
+- is_attacked:
+   0:
+      - is_navyExpenditure_available:
+         1:
+            - is_professionalarmy_researched:
+               1:
+                  - is_shipyard_level_enough:
+                     0: 
+                        - is_constructing:
+                           0: 
+                              - is_any_corruption:
+                                 0: 
+                                    - is_space_enough:
+                                       1:
+                                          - is_resource_enoghforHall:
+                                             0: build_shipyard
+                     1:
+                        - is_shipyard_upgrading:
+                           0:
+                              - is_navy_trainning:
+                                 0:
+                                    - train_navy:
+                                       211: buildShips_211
+                                       212: buildShips_212
+                                       213: buildShips_213
+                                       214: buildShips_214
+                                       215: buildShips_215
+                                       216: buildShips_216
+      - is_milityExpenditure_available:
+         1:
+            - is_professionalarmy_researched:
+               1:
+                  - is_barracks_level_enough:
+                     0:
+                        - is_constructing:
+                           0: 
+                              - is_any_corruption:
+                                 0: 
+                                    - is_space_enough:
+                                       1:
+                                          - is_resource_enoghforHall:
+                                             0: build_barracks
+                     1:
+                        - is_barracks_upgrading:
+                           0:
+                              - is_army_trainning:
+                                 0:
+                                    - train_army:
+                                       301: buildUnits_301
+                                       302: buildUnits_302
+                                       303: buildUnits_303
+                                       304: buildUnits_304
+                                       305: buildUnits_305
+                                       306: buildUnits_306
+                                       307: buildUnits_307
+                                       308: buildUnits_308
+                                       309: buildUnits_309
+                                       310: buildUnits_310
+                                       311: buildUnits_311
+                                       312: buildUnits_312
+                                       313: buildUnits_313
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/overall.yaml	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,32 @@
+---
+# country level management.
+- is_attacked:
+   # we are in Peace :D
+   0:
+     # 相關基礎研究 (technologies tree)
+     # 財富
+     - is_wealth_researched:
+        0: research_economy
+     # 擴張
+     - is_expansion_researched:
+        0: research_seafaring
+     # 造紙
+     - is_paper_researched:
+        0: research_knowledge
+     # 正規軍
+     - is_professionalarmy_researched:
+        0: resaerch_military
+     # 釀酒
+     - is_winepress_researched:
+        0: research_economy
+     # 發明 (兵工廠)
+     - is_invention_researched:
+        0: research_knowledge
+     # 異國文化, 建造博物館
+     - is_culturalexchange_researched:
+        0: research_knowledge
+     # 希臘火, 建造噴火船
+     - is_greekfire_researched:
+        0: research_seafaring
+
+   1: run_defence
--- a/pyikb/Parser.py	Thu Oct 23 00:47:56 2008 +0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-import re,string
-from sgmllib import SGMLParser  
-
-class ContentParser(SGMLParser):
-    def __init__(self):
-        SGMLParser.__init__(self)
-        self.anchor =  {'link':'', 'title':''}
-        self.anchorlist = []
-	self.liattr={}
-        self.inside_elements=['site']
-	self.pat=re.compile('\r|\t|\n')
-
-    def start_a(self, attributes):
-        """For each anchor tag, pay attention to the href and title attributes."""
-        href, title = '', ''
-        for name, value in attributes:
-            if name.lower() == 'href': href = value
-            if name.lower() == 'title': title = value
-        self.anchor['link'] = href
-        self.anchor['title'] = title
-        self.inside_elements.append('anchor')
-
-    def end_a(self):
-        self.anchorlist.append(self.anchor) # store the anchor in a list 
-        self.anchor = {'link':'', 'title':''}   # reset the dictionary,  
-        self.inside_elements.pop()
-
-    def handle_data(self, text):
-        if self.inside_elements[-1]=='anchor':
-            self.anchor['title'] = text
-	if self.inside_elements[-1]=='li':
-	    text=self.pat.sub(' ',text)
-	    text=string.split(text," ")
-	    if self.liattcl in self.liattr:
-	    	self.liattr[self.liattcl]=self.liattr[self.liattcl]+text
-	    else:
-	        self.liattr[self.liattcl]=text
-
-    def start_li(self,attributes):
-	self.liattcl=''
-        attrs = dict(attributes)
-	if attrs.has_key('class'):
-	     	self.liattcl=attrs['class']
-		self.inside_elements.append('li')
-
-    def end_li(self):
-	if self.inside_elements[-1]=='li':
-	    self.inside_elements.pop()
-	
--- a/pyikb/ikariam.py	Thu Oct 23 00:47:56 2008 +0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-import os,sys,re,string
-import cookielib,urllib2,urllib # for urlencode
-import time
-from lconf import LoadConfigfile
-from Parser import ContentParser
-
-class connection(object):
-    def __init__(self):
-	self.page=''
-	self.confdata=LoadConfigfile().cd
-	self.baseurl='http://'+self.confdata['server']
-        self.COOKIEFILE = '/tmp/ikcookies.lwp'
-	self.cj = cookielib.LWPCookieJar()
-	if os.path.isfile(self.COOKIEFILE):
-	    self.cj.load(self.COOKIEFILE)
-	opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cj))
-	opener.addheaders = [('User-agent', 'Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.8.1.12pre) Gecko/20071220 BonEcho/2.0.0.12pre')]
-	urllib2.install_opener(opener)
-
-    def login(self):
-        if not os.path.isfile(self.COOKIEFILE):
-	    print "create cookie file"+self.COOKIEFILE
-	    params = {"universe":self.confdata['server'], \
-	    "name":self.confdata['user'], \
-	    "password":self.confdata['pass']}
-
-            data = urllib.urlencode(params)
-            self.page=urllib2.urlopen(self.baseurl+'/index.php?action=loginAvatar&function=login',data).read()
-	self.cj.save(self.COOKIEFILE)
-	return 1
-
-    def parser(self):
-        parser=ContentParser()
-        parser.feed(self.page)
-        parser.close
-	for x in parser.liattr.keys():
-	    print x,parser.liattr[x]
-	#parser.anchorlist:
-
-    def logout(self):
-        logout=urllib2.urlopen(self.baseurl+'/index.php?action=loginAvatar&function=logout').read()
-	os.remove(self.COOKIEFILE)
-	return 1
-
-    def plunder(self):
-    	'/index.php?view=plunder&destinationCityId=1978'
-
-
-    def upgradetest(self):
-        urllib2.urlopen(self.baseurl+'/index.php?view=academy&id=117257&position=9').read()
-	params = {"action":'CityScreen', \
-	          "function":'upgradeBuilding', \
-		  "id":'117257',\
-		  "position":'9',\
-		  "level":'7',\
-		  "oldView":'academy'}
-	print urllib2.urlopen(self.baseurl+'/index.php?view=townHall&id=117257&position=0#upgrade',urllib.urlencode(params)).read()
-	return 1
-
-def help():
-        print ("Usage: %s [Option] [Channel] [second]") % os.path.basename(sys.argv[0])
-        print ("Option: ")
-	helplist=[
-	("-h","--help","show this usage message."),
-	("-g","--game","Login to the game")
-	]
-	helplist.sort()
-	for x in helplist:
-	    print ("\t%2s, %-25s %s" % x)
-
-if __name__=='__main__':
-    if len(sys.argv) == 1:
-	help()
-	sys.exit(2) # common exit code for syntax error
-    else:
-	arglist=sys.argv[1:]
-	if arglist[0] in ('--game','-g'):
-	     gc=connection()
-	     gc.login()
-	     gc.parser()
-	     gc.logout()
-
-
--- a/pyikb/lconf.py	Thu Oct 23 00:47:56 2008 +0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-import os,string
-class LoadConfigfile(object):
-    def __init__(self):
-	profile = os.environ["HOME"]+'/.eagleeye.pm'
-	self.cd={}
-	if os.path.isfile(profile):
-	    print "Loading Config file."
-	    cfile=open(profile,'r')
-	    for line in cfile.xreadlines():
-	    	if line[0:3]=='$::':
-		   con=string.split(line[3:-2])
-		   self.cd[con[0]]=con[2][1:-1]	   
-	else:
-	    print "File don't exist."
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyikriam/README	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,2 @@
+apt-get install python-libgmail python-lxml
+
--- a/pyikriam/__init__.py	Thu Oct 23 00:47:56 2008 +0800
+++ b/pyikriam/__init__.py	Mon Dec 01 15:32:44 2008 +0800
@@ -8,22 +8,36 @@
 
     cities = {}
 
-    def __init__(self):
+    def __init__(self, DEBUG=False):
         self.COOKIEFILE = '/tmp/ikariam.lwp'
-	self.confdata=LoadConfigfile().cd
-        self.baseurl='http://'+self.confdata['server']
-        self.cj = cookielib.LWPCookieJar()
-        if os.path.isfile(self.COOKIEFILE):
-            self.cj.load(self.COOKIEFILE)
- 
-        opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cj))
-        opener.addheaders = [('User-agent', 'Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.8.1.12pre) Gecko/20071220 BonEcho/2.0.0.12pre')]
-        urllib2.install_opener(opener)
+        self.confdata=LoadConfigfile().cd
+        #self.baseurl='http://'+self.confdata['server']
+        if DEBUG:
+            self.debug = True
+            self.baseurl = self.debug_url()
+        else:
+            self.baseurl = 'http://s4.ikariam.tw'
+            self.cj = cookielib.LWPCookieJar()
+            if os.path.isfile(self.COOKIEFILE):
+                self.cj.load(self.COOKIEFILE)
+     
+            opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cj))
+            opener.addheaders = [('User-agent', 'Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.8.1.12pre) Gecko/20071220 BonEcho/2.0.0.12pre')]
+            urllib2.install_opener(opener)
+    
+            self.login()
 
-        self.login()
+    def debug_url(self):
+        """
+        get the path of testing datas.
+        >>>
+        
+        @return: file path with scheme 'file://'
+        """
+        return 'file://'+ os.path.abspath(os.path.curdir) + '/tests'
 
     def login(self):     
-	print "login to %s...." % self.confdata['server']
+        print "login to %s...." % self.confdata['server']
         params = {"universe":self.confdata['server'], \
         "name":self.confdata['user'], \
         "password":self.confdata['pass']}
@@ -31,12 +45,19 @@
         self.cj.save(self.COOKIEFILE)
         
     def logout(self):
-	print "logut from %s...." % self.confdata['server']
+        print "logut from %s...." % self.confdata['server']
         c(self.baseurl+'/index.php?action=loginAvatar&function=logout')
         os.remove(self.COOKIEFILE)
         
+    def find_number_in(self, html, xpath):    
+        return parse_to_int(self.find_data_in(html, xpath))
+    
+    def find_data_in(self, work, xpath):
+        if work is None or xpath is None:   return
+        return work.find(xpath)
+    
     def city(self, id):
-	return self.cities.get(id, IkariamCity(id=id, core=self) )
+	       return self.cities.get(id, IkariamCity(id=id, core=self) )
     
 class IkariamCity:
     
@@ -44,11 +65,30 @@
         self.core = core
         self.id = id
         self.params = {'view':'city','id':id}
+        self.resources = {'gold':0,'wood':0,'wine':0,'marble':0,'crystal':0,'sulfur':0}
+        
+        # define xpath queries
+        self.xpath = {
+                      'gold':"/html/body[@id='city']/div[@id='container']/div[@id='container2']/div[@id='globalResources']/ul/li[2]/a/span[@id='value_gold']/text()",
+                      'wood':"/html/body[@id='city']/div[@id='container']/div[@id='container2']/div[@id='cityResources']/ul/li[3]/span[@id='value_wood']/text()",
+                      'wine':"/////div[@id='cityResources']/ul/li[4]/span[@id='value_wine']/text()",
+                      'marble':"/////div[@id='cityResources']/ul/li[4]/span[@id='value_wine']/text()",
+                      'crystal':"/////div[@id='cityResources']/ul/li[4]/span[@id='value_wine']/text()",
+                      'sulfur':"/////div[@id='cityResources']/ul/li[4]/span[@id='value_wine']/text()"
+                      }
         
     def sync(self):
-	print "pull datas of the city %s" % self.id
-        xpath_globalinfo = "/html/body[@id='city']/div[@id='container']/div[@id='container2']/div[@id='globalResources']/ul"
-
-        xpath_gold = xpath_globalinfo + "/li[2]/a/span[@id='value_gold']/text()"
-        self.gold = c(self.core.baseurl).get(self.params).find(xpath_gold).get_content()[0]
+        print "pull datas of the city %s" % self.id
+                                
+        if self.core.debug:
+            work = c(self.core.baseurl+'/viewcity.html').get()
+        else:
+            work = c(self.core.baseurl).get(self.params)
+            
+        #print work.find(self.xpath['gold']).get_content()
+        print work.find(self.xpath['wood']).get_content()
+        print work.find(self.xpath['wood']).get_content()
         
+def parse_to_int( str ):    pass
+   # print str[0]
+   # if str: return int( str.strip().replace(',',''))
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyikriam/buildings.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,275 @@
+from lazy.www import c, Resource
+from lxml import etree
+from StringIO import StringIO
+from sync_utils import sync_tagclass, sync_tagvalue
+from sync_utils import sync_tagcount, sync_tagclass_start_appear
+from sync_utils import ikariam_zh_timeval
+
+class position(object):
+    def __init__(self, build_type, city_id, idx, baseurl):
+        self._baseurl = baseurl + '/index.php'
+        self.build_type = build_type
+        self.city_id = city_id
+        self.idx = idx
+        self._params = {'view': 'buildingGround',
+                        'id': city_id,
+                        'position': idx}
+        pass
+
+    def get_page(self):
+        page = c(self._baseurl).get(self._params).get_content()
+        return page
+
+    def sync(self):
+        page = self.get_page()
+        parser = etree.HTMLParser(encoding='utf8')
+        page_dom = etree.parse(StringIO(page), parser)
+
+        self._sync(page_dom)
+        pass
+    pass
+
+## \brief Base class of all building class.
+#
+# This class extract information from page of building.  That are
+# information all buildings have.
+#
+class building(position):
+    class_patterns = {
+        'level': 'buildingLevel'
+        }
+    appear_patterns = {
+        'is_upgrading': 'isUpgrading'
+        }
+    upgrade_res_patterns = {
+        'wood': 'wood',
+        'marble': 'marble',
+        'crystal': 'glass'
+        }
+
+    def _sync(self, page_dom):
+        sync_tagclass(self, building.class_patterns, page_dom)
+
+        sync_tagclass_start_appear(self, building.appear_patterns, page_dom)
+
+        xpath_upgrade = '/descendant::ul[@class=\'actions\']/li[@class=\'upgrade\']/a'
+        anodes = page_dom.xpath(xpath_upgrade)
+        if len(anodes) == 1:
+            anode = anodes[0]
+            self._upgrade_uri = anode.get('href')
+        else:
+            self._upgrade_uri = None
+            pass
+
+        self.upgrade_wood = 0
+        self.upgrade_marble = 0
+        self.upgrade_crystal = 0
+        self.upgrade_time = 0
+        self.upgrade_countdown = 0
+
+        if self.is_upgrading:
+            xpath_countdown = '/descendant::div[@id=\'upgradeCountDown\']/text()'
+            value = page_dom.xpath(xpath_countdown)[0]
+            self.upgrade_countdown = ikariam_zh_timeval(value)
+        else:
+            xpath_res = '/descendant::div[@class=\'content\']/ul[@class=\'resources\']/li[starts-with(@class, \'%s\')]/text()'
+
+            for resname, clzname in building.upgrade_res_patterns.items():
+                xpath = xpath_res % (clzname)
+                txts = page_dom.xpath(xpath)
+                if len(txts) == 1:
+                    value = txts[0].strip()
+                    setattr(self, 'upgrade_' + resname, int(value))
+                    pass
+                pass
+
+            xpath_time = xpath_res % ('time')
+            txts = page_dom.xpath(xpath_time)
+            if len(txts) == 1:
+                value = txts[0].strip()
+                self.upgrade_time = ikariam_zh_timeval(value)
+                pass
+            pass
+        pass
+
+    def upgrade(self):
+        url = self._baseurl + self._upgrade_uri
+        page = c(url).get().get_content()
+        pass
+    pass
+
+class townhall(building):
+    class_patterns = {
+        'occupied': 'value occupied',
+        'rooms': 'value total',
+        }
+    value_patterns = {
+        'growth': 'growth',
+        'happiness': 'happiness',
+        'interest_base': 'base',
+        'interest_research': 'research1',
+        'interest_capital': 'capital',
+        'overpopulation': 'cat overpopulation'
+        }
+    count_patterns = {
+        'pop_citizens': 'citizens',
+        'pop_woodworkers': 'woodworkers',
+        'pop_specialworkers': 'specialworkers',
+        'pop_scientists': 'scientists'
+        }
+    
+    def __init__(self, city_id, idx, baseurl):
+        super(townhall, self).__init__('townhall', city_id, idx, baseurl)
+        pass
+
+    def _sync(self, page_dom):
+        sync_tagclass(self, townhall.class_patterns, page_dom)
+
+        sync_tagvalue(self, townhall.value_patterns, page_dom)
+
+        sync_tagcount(self, townhall.count_patterns, page_dom)
+
+        super(townhall, self)._sync(page_dom)
+        pass
+    pass
+
+class academy(building):
+    def __init__(self, city_id, idx, baseurl):
+        super(academy, self).__init__('academy', city_id, idx, baseurl)
+        pass
+
+    def _sync(self, page_dom):
+        xpath_research_name = '/descendant::*[@class=\'researchName\']/a'
+        anodes = page_dom.xpath(xpath_research_name)
+        if len(anodes) == 1:
+            anode = anodes[0]
+            self.researching = anode.get('title')
+            xpath_countdown = '/descendant::div[@id=\'researchCountDown\']/text()'
+            txtnodes = page_dom.xpath(xpath_countdown)
+            self.researching_countdown = ikariam_zh_timeval(txtnodes[0])
+        else:
+            self.researching = None
+            self.researching_countdown = None
+            pass
+
+        super(academy, self)._sync(page_dom)
+        pass
+    pass
+
+class warehouse(building):
+    def __init__(self, city_id, idx, baseurl):
+        super(warehouse, self).__init__('warehouse', city_id, idx, baseurl)
+        pass
+    pass
+
+class barracks(building):
+    def __init__(self, city_id, idx, baseurl):
+        super(barracks, self).__init__('barracks', city_id, idx, baseurl)
+        pass
+    pass
+
+class branchoffice(building):
+    def __init__(self, city_id, idx, baseurl):
+        super(branchoffice, self).__init__('branchoffice', city_id, idx, baseurl)
+        pass
+    pass
+
+class port(building):
+    def __init__(self, city_id, idx, baseurl):
+        super(port, self).__init__('port', city_id, idx, baseurl)
+        pass
+    pass
+
+class wall(building):
+    def __init__(self, city_id, idx, baseurl):
+        super(wall, self).__init__('wall', city_id, idx, baseurl)
+        pass
+    pass
+
+class shipyard(building):
+    def __init__(self, city_id, idx, baseurl):
+        super(shipyard, self).__init__('shipyard', city_id, idx, baseurl)
+        pass
+    pass
+
+class safehouse(building):
+    def __init__(self, city_id, idx, baseurl):
+        super(shipyard, self).__init__('safehouse', city_id, idx, baseurl)
+        self.data_patterns = {
+                              'vacancy_spys_total':"/::input[@id='spyCount']/@value",
+                              'trained_spys_total':"/descendant::div[@id='mainview']/div[4]/div[1]/p/span/span[2]/text()"}
+    pass
+
+    def __sync__(self, page_dom):        
+        workflow = create_workflow('find', page_dom)
+        attrs = workflow.findall(self.data_patterns).get_content()
+        for attr_name, attr_value in attrs:
+            setattr(self, attr_name, attr_value)
+        pass
+    pass
+
+    def _mk_spy(self):
+        import spys
+
+class empty_pos(position):
+    res_patterns = {
+        'wood': 'wood',
+        'marble': 'marble',
+        'crystal': 'glass'
+        }
+
+    def _sync(self, page_dom):
+        self.building_info = None
+        self.res_wood = 0
+        self.res_marble = 0
+        self.res_crystal = 0
+        self.building_time = 0
+        self._building_uri = None
+
+        xpath_building = '/descendant::div[@class=\'buildinginfo\']/h4/text()'
+        buildings = page_dom.xpath(xpath_building)
+        if len(buildings) == 1:
+            self.building_info = buildings[0]
+            pass
+
+        xpath_costs = '/descendant::div[@class=\'costs\']/ul/li[@class=\'%s\']/text()'
+        for res, ptn in empty_pos.res_patterns.items():
+            xpath = xpath_costs % (ptn)
+            txts = page_dom.xpath(xpath)
+            if len(txts) == 1:
+                value = int(txts[0])
+                setattr(self, 'res_' + res, value)
+                pass
+            pass
+
+        xpath = xpath_costs % ('time')
+        txts = page_dom.xpath(xpath)
+        if len(txts) == 1:
+            value = ikariam_zh_timeval(txts[0])
+            self.building_time = value
+            pass
+
+        xpath_button = '/descendant::a[@class=\'button build\']'
+        anodes = page_dom.xpath(xpath_button)
+        if len(anodes) == 1:
+            self._building_uri = anodes[0].get('href')
+            pass
+        pass
+
+    def build(self):
+        url = self._baseurl + self._building_uri
+        page = c(url).get().get_content()
+        pass
+    pass
+
+class land(empty_pos):
+    def __init__(self, city_id, idx, baseurl):
+        super(land, self).__init__('land', city_id, idx, baseurl)
+        pass
+    pass
+
+class shore(empty_pos):
+    def __init__(self, city_id, idx, baseurl):
+        super(shore, self).__init__('shore', city_id, idx, baseurl)
+        pass
+    pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyikriam/createAccount.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,75 @@
+import urllib2,urllib
+import libgmail,email,string,time
+from ikariam import fake_moz
+from lazy.www import c
+
+class CreateAccount:
+    """
+    For create sheep to cheat. take care of the ip address & email check by yourself.
+    """
+    def __init__(self,account,password,server):
+        self.account=account
+        self.password=password
+        if len(self.password)<8:
+            print 'Password size mast bigger then 8 chars'
+            pass
+        self.server=server
+        headers=[('Referer','http://'+self.server[self.server.find('.')+1:]+'/register.php')]
+        self.browser = fake_moz(headers)
+        urllib2.install_opener(self.browser)
+        pass
+
+    def ca(self):
+        self.baseurl='http://'+self.server
+        params = {
+        "function":"createAvatar",\
+        "name":self.account,\
+        "email":self.account+"@ossug.org",\
+        "password":self.password,\
+        "agb":"on"}
+        self.ret=c(self.baseurl+'/index.php?action=newPlayer').get(params).get_content()
+        print "Waiting for 30 seconds to get confirme..."
+        time.sleep(30)
+        confirme(self.account).run()
+        pass
+
+class confirme:
+    def __init__(self,account):
+        self.account=account
+        self.browser = fake_moz()
+        urllib2.install_opener(self.browser)
+        pass
+
+    def run(self):
+        print "Start get confirme url..."
+        ga = libgmail.GmailAccount("gmp3fs@gmail.com", "ossug.org")
+        ga.login()
+        folder = ga.getMessagesByFolder('inbox')
+
+        for thread in folder:
+            #thread.id, len(thread), thread.subject
+            account=string.split(thread.subject)[0]
+            acsize=len(self.account)
+            if account[-acsize:]==self.account:
+                print "find "+account
+                for msg in thread:
+                    mmsg=email.message_from_string(msg.source)
+                    confcontext=mmsg.get_payload(decode = True)
+                    for x in string.split(confcontext):
+                        if x[0:7]=='http://':
+                            confurl=x
+                            break
+        print "send confirme."
+        self.ret=c(confurl).get().get_content()
+        pass
+
+    def rename(self):
+        """
+        http://s4.ikariam.tw/index.php?view=renameCity&id=6079&position=0
+        <input class="textfield" id="newCityName" name="name" size="30" maxlength="15" type="text">
+        <input class="button" value="" type="submit">
+        """
+
+ikariamca=CreateAccount('jdanny','taaaa123','s4.ikariam.tw')
+a=ikariamca.ca()
+
--- a/pyikriam/example.py	Thu Oct 23 00:47:56 2008 +0800
+++ b/pyikriam/example.py	Mon Dec 01 15:32:44 2008 +0800
@@ -1,6 +1,37 @@
-from __init__ import Ikariam
+import sys
+import buildings
+from ikariam import Ikariam
+
+if len(sys.argv) != 2:
+    print >> sys.stderr, 'Usage: %s <city id>' % (sys.argv[0])
+    sys.exit(1)
+    pass
+
+city_id = int(sys.argv[1]) # 117261
 
 i = Ikariam()
-city = i.city(117261)
+city = i.city(city_id)
+
+print "pull datas of the city %s" % (city_id)
 city.sync()
-print 'gold is'+city.gold
+
+print 'gold is ' + city.gold
+print 'inhabitants is ' + city.inhabitants
+print 'wood is ' + city.wood
+print 'wine is ' + city.wine
+print 'marble is ' + city.marble
+print 'crystal is ' + city.crystal
+print 'sulfur is ' + city.sulfur
+
+for idx, pos in enumerate(city.positions):
+    if not isinstance(pos, buildings.position):
+        continue
+    pos.sync()
+    building_attrs  = filter(lambda attr: not attr[0].startswith('_'),
+                             pos.__dict__.items())
+    building_attrs.sort(key=lambda x: x[0])
+    print
+    print 'positions[%d]' % (idx)
+    for building_attr, value in building_attrs:
+        print '\t%s: %s' % (building_attr, repr(value))
+        pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyikriam/ikariam.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,121 @@
+from lazy.www import c
+from lconf import LoadConfigfile
+import cookielib
+import os
+import urllib2
+import urllib
+from utils import dyna_prog, decorator
+from lxml import etree
+from StringIO import StringIO
+
+class fake_moz(object):
+    __metaclass__ = decorator
+
+    def __init__(self,headers=None):
+        fakeheaders=[('User-agent', 'Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.8.1.12pre) Gecko/20071220 BonEcho/2.0.0.12pre')]
+        if headers:
+            fakeheaders=fakeheaders+headers
+        super(fake_moz, self).__init__()
+        cookie_jar = cookielib.LWPCookieJar()
+        cookie_proc = urllib2.HTTPCookieProcessor(cookie_jar)
+        opener = urllib2.build_opener(cookie_proc)
+        opener.addheaders = fakeheaders 
+        fake_moz.set_backend(self, opener)
+        self.cookie_jar = cookie_jar
+        pass
+    pass
+
+class Ikariam:
+
+    cities = {}
+    COOKIEFILE = '/tmp/ikariam.lwp'
+
+    def __init__(self):
+        self.browser = fake_moz()
+        self._cookie_jar = self.browser.cookie_jar
+
+        if os.path.isfile(self.COOKIEFILE):
+            self._cookie_jar.load(self.COOKIEFILE)
+            pass
+ 
+        urllib2.install_opener(self.browser)
+	self.confdata=LoadConfigfile().cd
+        self.baseurl='http://'+self.confdata['server']
+
+        self.login()
+        pass
+
+    def login(self):     
+	print "login to %s...." % self.confdata['server']
+        params = {"universe":self.confdata['server'], \
+        "name":self.confdata['user'], \
+        "password":self.confdata['pass']}
+        ret = c(self.baseurl+'/index.php?action=loginAvatar&function=login').get(params).get_content()
+        self._cookie_jar.save(self.COOKIEFILE)
+        pass
+
+    def logout(self):
+	print "logut from %s...." % self.confdata['server']
+        c(self.baseurl+'/index.php?action=loginAvatar&function=logout')
+        os.remove(self.COOKIEFILE)
+        pass
+    
+    ##
+    # \note We can cache data with decorator 'dynamic programming'.
+    #
+    @dyna_prog
+    def city(self, id):
+        return IkariamCity(id=id, core=self)
+    pass
+
+class IkariamCity:
+    data_patterns = {
+        'gold': 'value_gold',
+        'inhabitants': 'value_inhabitants',
+        'wood': 'value_wood',
+        'wine': 'value_wine',
+        'marble': 'value_marble',
+        'crystal': 'value_crystal',
+        'sulfur': 'value_sulfur'
+        }
+    def __init__(self, id, core ):
+        self.core = core
+        self.id = id
+        self.params = {'view':'city','id':id}
+        
+    def sync(self):
+        page = c(self.core.baseurl).get(self.params).get_content()
+        parser = etree.HTMLParser(encoding='utf8')
+        page_dom = etree.parse(StringIO(page), parser)
+
+        xpath_globalinfo = '/descendant::*[@id=\'%s\']/text()'
+        for name, tag_id in self.data_patterns.items():
+            xpath = xpath_globalinfo % (tag_id)
+            value = page_dom.xpath(xpath)[0]
+            setattr(self, name, value)
+            pass
+        
+        xpath_mainview = '/descendant::div[@id=\'mainview\']/ul/li'
+        pos_doms = page_dom.xpath(xpath_mainview)
+        positions = [self._mk_position(pos_dom, idx)
+                     for idx, pos_dom in enumerate(pos_doms)]
+        self.positions = positions
+        pass
+
+    def _mk_position(self, pos_dom, idx):
+        import buildings
+
+        build_type = pos_dom.get('class').split()[-1].lower()
+        if hasattr(buildings, build_type):
+            clz = getattr(buildings, build_type)
+            if issubclass(clz, buildings.position):
+                building = clz(self.id, idx, self.core.baseurl)
+                return building
+            pass
+        return build_type
+    pass
+
+        
+        
+        
+            
\ No newline at end of file
--- a/pyikriam/lazy/www/__init__.py	Thu Oct 23 00:47:56 2008 +0800
+++ b/pyikriam/lazy/www/__init__.py	Mon Dec 01 15:32:44 2008 +0800
@@ -15,7 +15,7 @@
         return {'msg':s}
     
     try:
-        c("http://example.tw/").get().find("#id > div") \
+        c("http://example.tw/").get().find("////ul/text()") \
             .build_param( func ).post_to("http://example2.com") \
             .save_as('hellow.html')
     except:
@@ -38,10 +38,21 @@
         print "unknow error."
 """
 from lazy.www.work import WorkFlow
-from lazy.www.work.fetch import Fetcher, install_opener
+from lazy.www.work.fetch import Fetcher
+from lazy.www.work.storage import FileStorager
 from lazy.www.core import SemiProduct
+import os
+import sys
+import re
 
-def c(url):
+def parse_scheme(scheme):
+    try:
+        return re.findall("(\w+):\/\/(.*\/?)",scheme)[0]
+    except:
+        sys.stdout.write("the scheme is not supported.")
+        sys.exit()
+
+def c(scheme):
     """
     connect to a web apge
     
@@ -51,14 +62,17 @@
     >>> c('http://localhost:8080').get().find('//text()')
     'It works!!\\n'    
     """
-    s= SemiProduct(source=url)    
-    w = WorkFlow(Fetcher(s))
-    return w
+    target_type, path = parse_scheme(scheme)
 
-def lz_install(**kwds):
-    if('opener' == kwds.get('name')):
-       install_opener(kwds.get('cookiefile'))
+    #@todo: SemiProduct Factory.
+    if 'file' == target_type:
+        s= SemiProduct(source=path)        
+        worker = FileStorager(s)
+    else:
+        s= SemiProduct(source=scheme)        
+        worker = Fetcher(s)
+    return WorkFlow(worker)
 
 if __name__ == '__main__':
     import doctest
-    doctest.testmod()
\ No newline at end of file
+    doctest.testmod()
--- a/pyikriam/lazy/www/core/__init__.py	Thu Oct 23 00:47:56 2008 +0800
+++ b/pyikriam/lazy/www/core/__init__.py	Mon Dec 01 15:32:44 2008 +0800
@@ -9,4 +9,38 @@
         self.source = kwds.get('source','')        
         
     def __str__(self):        
-        return self.content
\ No newline at end of file
+        return self.content
+    
+class Resource(object):
+
+    datas = {}
+    
+    data_patterns = {}
+    
+    def __init__(self, **kwds):
+        self.datas = kwds.get('datas')
+        
+    def __get__(self, key):
+        try:
+            return self.datas[key]
+        except KeyError:
+            return self.key
+        
+    def __set__(self, key, value):
+        try:
+            self.datas[key]
+        except KeyError:
+            self.key = value
+            
+    def sync(self):
+        if not self.data_patterns:  raise AttributeError("data patterns not defined.")
+        
+        express = {}
+        for attr in self.attr_xpath.keys():
+            express[attr] = self.root_xpath+self.attr_xpath[attr]
+
+        def fn(e):
+            for x in e: e[x] = e[x][0]
+            return e
+
+        self.datas = self.core.open(self.param).findall(express).process(fn).get_content()        
\ No newline at end of file
--- a/pyikriam/lazy/www/work/__init__.py	Thu Oct 23 00:47:56 2008 +0800
+++ b/pyikriam/lazy/www/work/__init__.py	Mon Dec 01 15:32:44 2008 +0800
@@ -21,14 +21,15 @@
         return self.working_product.content
      
     def change_worker(self, new_worker):
-        self.serial_number += 1
+        self.serial_number += 1        
+        self.last_work = self
         self.worker = new_worker
         
     def is_fetcher(self, obj):
-        if  obj is not None:    return True
+        if obj.__class__.__name__ == 'Fetcher': return True
     
     def get(self, data = {} ):
-        if not self.is_fetcher(self.worker) :
+        if self.worker.__class__.__name__ != 'FileStorager' and not self.is_fetcher(self.worker) :
             self.change_worker( Fetcher(self.working_product) )
         
         self.working_product.content = self.worker.get(data)
@@ -42,11 +43,31 @@
         return self
     
     def is_finder(self, obj):
-        if obj is not None: return True
+        if obj.__class__.__name__ == 'Finder': return True
+    
+    def findall(self, expresses):
+        if not self.is_finder(self.worker):
+            self.change_worker( Finder(self.working_product) )
+        
+        ret = {}
+        for e in expresses.keys():
+            try:
+                ret[e] = self.worker.find(expresses[e])
+            except:
+                pass
+
+        self.working_product.content = ret
+        
+        return self
     
     def find(self, express):
         #if not self.is_finder(self.worker):
-        self.worker = Finder(self.working_product)
+        self.change_worker( Finder(self.working_product) )
+        self.last_working_product = self.working_product
         self.working_product.content = self.worker.find(express)
         
+        return self
+    
+    def process(self, fn):
+        self.working_product.content = fn(self.working_product.content)
         return self
\ No newline at end of file
--- a/pyikriam/lazy/www/work/fetch.py	Thu Oct 23 00:47:56 2008 +0800
+++ b/pyikriam/lazy/www/work/fetch.py	Mon Dec 01 15:32:44 2008 +0800
@@ -3,17 +3,6 @@
 import cookielib
 import os
 
-def install_opener(cookiefile):
-    COOKIEFILE = cookiefile
-    cj = cookielib.LWPCookieJar()
-    if os.path.isfile(COOKIEFILE):
-        cj.load(COOKIEFILE)
-    else:
-        cj.save(cookiefile)
-        opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
-        opener.addheaders = [('User-agent', 'Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.8.1.12pre) Gecko/20071220 BonEcho/2.0.0.12pre')]
-        urllib2.install_opener(opener)
-
 class Fetcher:
     
     opener = None
@@ -31,15 +20,14 @@
     """
     def __init__(self, working_product):
         self.working_product = working_product
-        
-    def get(self, data = {}):
-        """        
-        send datas via http get method.
-        """        
-        res = urllib2.urlopen(self.working_product.source, urllib.urlencode(data))
-        return res.read()
     
-    def post(self, data = {} ):
+    def get(self, **kwds):
+        return self.open(kwds)
+
+    def post(self, **kwds):
+        return self.open(kwds)
+    
+    def open(self, data = {} ):
         """
         send datas via http post method.
         
--- a/pyikriam/lazy/www/work/find.py	Thu Oct 23 00:47:56 2008 +0800
+++ b/pyikriam/lazy/www/work/find.py	Mon Dec 01 15:32:44 2008 +0800
@@ -10,14 +10,18 @@
         self.working_prodcut = working_product
 
         self.encoding = 'utf8'
-        parser = etree.HTMLParser(encoding=self.encoding)
-        self.dom_tree = etree.parse(StringIO(self.working_prodcut.content), parser)
     
     def find(self, express , callback = None):
+        
+        if self.dom_tree is None:   self.set_dom_tree(self.working_prodcut.content)
+ 
         xpath = self.dom_tree.xpath(express)
         
-        if callback is None:
-            ret = xpath
-        else:
-            ret = self.callback(xpath)
-        return ret
+        if callback:    return self.callback(xpath)
+        return xpath
+
+    def set_dom_tree(self, content):
+        stream = StringIO(content)
+
+        parser = etree.HTMLParser(encoding=self.encoding)
+        self.dom_tree = etree.parse(stream, parser)
\ No newline at end of file
--- a/pyikriam/lconf.py	Thu Oct 23 00:47:56 2008 +0800
+++ b/pyikriam/lconf.py	Mon Dec 01 15:32:44 2008 +0800
@@ -1,15 +1,33 @@
-import os,string
+import os, string
+import re
+
+_user_home = os.environ['HOME']
+dfl_profile = os.path.join(_user_home, '.eagleeye.pm')
+_reo_assign = re.compile('^\\$::([_a-zA-Z][_a-zA-Z0-9]+) *= *([\'"][^"]*[\'"]).*$')
+
+def _real_load_conf(conf_o):
+    confs = [_reo_assign.match(line)
+             for line in conf_o
+             if line.startswith('$::')]
+    cd = dict([(mo.group(1), eval(mo.group(2)))
+               for mo in confs if mo])
+    return cd
+
 class LoadConfigfile(object):
-    def __init__(self):
-	profile = os.environ["HOME"]+'/.eagleeye.pm'
-	self.cd={}
-	if os.path.isfile(profile):
-	    print "Loading Config file."
-	    cfile=open(profile,'r')
-	    for line in cfile.xreadlines():
-	    	if line[0:3]=='$::':
-		   con=string.split(line[3:-2])
-		   self.cd[con[0]]=con[2][1:-1]	   
-	else:
-	    print "File don't exist."
+    def __init__(self, profile=dfl_profile):
+        prof_o = open(profile, 'r')
+        self.cd = _real_load_conf(prof_o)
+        pass
+    pass
 
+if __name__ == '__main__':
+    from StringIO import StringIO
+    conf = '''
+$::user = "alsfjsf"; #lsfjslf
+$::server = "fkljalfasf"; # sfjslf
+$::pass = "lsfjslfsf";
+'''
+    conf_o = StringIO(conf)
+    cd = _real_load_conf(conf_o)
+    print cd
+    pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyikriam/sync_utils.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,72 @@
+## \file
+# \brief Sync information of objects with DOM trees of respective pages.
+#
+
+import re as _re
+
+def sync_tagclass(obj, patterns, page_dom):
+    xpath_building = '/html/body/descendant::*[@class=\'%s\']/text()'
+    for name, clzname in patterns.items():
+        path = xpath_building % (clzname)
+        value = float(page_dom.xpath(path)[0])
+        setattr(obj, name, value)
+        pass
+    pass
+
+def sync_tagvalue(obj, patterns, page_dom):
+    xpath_value = '/html/body/descendant::*[starts-with(@class,\'%s\')]/descendant::*[@class=\'value\']/text()'
+    for name, clzname in patterns.items():
+        path = xpath_value % (clzname)
+        value = float(page_dom.xpath(path)[0])
+        setattr(obj, name, value)
+        pass
+    pass
+
+def sync_tagcount(obj, patterns, page_dom):
+    xpath_count = '/html/body/descendant::*[starts-with(@class,\'%s\')]/descendant::*[@class=\'count\']/text()'
+    for name, clzname in patterns.items():
+        path = xpath_count % (clzname)
+        value = int(page_dom.xpath(path)[0])
+        setattr(obj, name, value)
+        pass
+    pass
+
+def sync_tagclass_start_appear(obj, patterns, page_dom):
+    xpath_appear = '/html/body/descendant::*[starts-with(@class,\'%s\')]'
+    for name, clzname in patterns.items():
+        path = xpath_appear % (clzname)
+        cnt = len(page_dom.xpath(path))
+        if cnt != 0:
+            setattr(obj, name, True)
+        else:
+            setattr(obj, name, False)
+            pass
+        pass
+    pass
+
+_reo_tv = _re.compile(u'(([0-9]+)\u6642)? ?(([0-9]+)\u5206)? ?(([0-9]+)\u79d2)?')
+## \brief Translate timeval in Chinese text format to integer seconds.
+#
+def ikariam_zh_timeval(tv_str):
+    tmo = _reo_tv.match(tv_str)
+    if not tmo:
+        raise SyntaxError, \
+            '%s is an invalid time interval string' % (repr(tv_str))
+    tv = 0
+
+    value = tmo.group(2)        # hour
+    if value:
+        tv = tv + int(value) * 3600
+        pass
+    
+    value = tmo.group(4)        # minute
+    if value:
+        tv = tv + int(value) * 60
+        pass
+    
+    value = tmo.group(6)        # second
+    if value:
+        tv = tv + int(value)
+        pass
+
+    return tv
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyikriam/utils.py	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,80 @@
+## \brief decorator is a class decorate another of another object.
+#
+# Access of attributes/methods that can not be found in an instance
+# of a class of this meta-class, decorator, are delegated to the backend
+# of the instance.
+#
+# A backend object always passed as first parameter when
+# instantiate an instance of a class with decorator as meta-class.
+# For example,
+# \code
+# class foo(object):
+#     __metaclass__ = decorator
+#
+# backend.data = 3
+# obj = foo(backend)
+# print obj.data
+# backend.data = 4
+# print obj.data
+# \endcode
+# it print out 3 and 4 respectively.
+# 
+class decorator(type):
+    def __init__(clz, name, base, dict):
+        super(decorator, clz).__init__(name, base, dict)
+        clz.__getattr__ = decorator.__dele_getattr
+        pass
+
+    @staticmethod
+    def set_backend(obj, backend):
+        obj.__backend = backend
+        pass
+
+    @staticmethod
+    def __dele_getattr(obj, name):
+        return getattr(obj.__backend, name)
+    pass
+
+
+## \brief Decorator to make functions or methods dynamic programming.
+#
+# dyna_prog result of functions or methods with their arguments as key.
+# It supposes result of a function always the same if the same arguments
+# are passed.  It cache result of cached function to avoid really calling
+# function every time.
+class dyna_prog(object):
+    class functor(object):
+        def __init__(self, instance, method):
+            self._method = method
+            self._instance = instance
+            self._cache = {}
+            pass
+
+        def __call__(self, *args):
+            try:
+                return self._cache[args]
+            except KeyError:
+                pass
+            instance = self._instance
+            result = self._method(instance, *args)
+            return self._cache.setdefault(args, result)
+
+        def clear(self):
+            self._cache.clear()
+        pass
+
+    def __init__(self, method):
+        super(dyna_prog, self).__init__()
+        self._method = method
+        self._functors = {}
+        pass
+    
+    def __get__(self, instance, owner):
+        try:
+            return self._functors[instance]
+        except KeyError:
+            pass
+        functor_o = dyna_prog.functor(instance, self._method)
+        return self._functors.setdefault(instance, functor_o)
+    pass
+
--- a/scan.pl	Thu Oct 23 00:47:56 2008 +0800
+++ b/scan.pl	Mon Dec 01 15:32:44 2008 +0800
@@ -1,9 +1,11 @@
 #!/usr/bin/perl
 use strict;
 use Data::Dumper;
+use Carp;
 use Ikariam;
 
 package main;
+
 sub saveCities
 {
     my ($island, @cities) = @_;
@@ -13,108 +15,125 @@
         return;
     }
 
-    foreach my $city (@cities)
-    {
-        $city->{island} = $island;
-        $city->{'time'} = time;
-        if(my $c = Ikariam::Cities->retrieve($city->{cityId}))
-        {
-            foreach my $i (keys(%$city)) {
-                eval($c->$i($city->{$i}));
-                # printf("%s %s ", $i, $city->{$i});
+    foreach my $h_city (@cities) {
+# Carp::carp(sprintf("checking %s\n", $h_city->{'cityname'}));
+        $h_city->{island} = $island;
+        $h_city->{'time'} = time;
+        my $c = Ikariam::Cities->retrieve($h_city->{cityId});
+        if(defined($c)) {
+            foreach (keys(%$h_city)) { 
+                $c->set($_ => $h_city->{$_}); 
             }
-            # print ("\n");
-
-            $c->autoupdate(1);
-            $c->update();
         } else {
-            Ikariam::Cities->insert($city);
+            $c = Ikariam::Cities->insert($h_city);
         }
-
-        printf("city %s (%d) at island (%d) saved\n", 
-            $city->{cityname},
-            $city->{cityId}, 
-            $island);
+        $c->update();
 
-        my $user = Ikariam::User->retrieve($city->{user});
-        if(!defined($user) || $user->time le (time - 60*60*1)) {
-            # Download user profile.
-            foreach my $x (qw/score army_score_main trader_score_secondary/)
-            {
-                my $users = $::i->viewScore($x, $city->{owner}, 0);
-                saveUser($users);
-            }
-        }
+        my $user = Ikariam::User->retrieve($h_city->{'user'});
+        next if (defined($user) && defined($user->time) && $user->time gt (time - 30*60));
+
+        saveUser($h_city->{owner});
     }
 }
 
 sub saveUser
 {
-    my $users = shift;
+    my $userName = shift;
+    my $users;
+
+    foreach my $x (qw/score army_score_main trader_score_secondary/) {
+        $users = $::i->viewScore($x, $userName, 0);
+
+        foreach my $h_user (values(%{$users})) {
+# Carp::carp(sprintf("Saving user %s\n", $h_user->{'name'}));
+            $h_user->{'time'} = time;
 
-    foreach my $h_user (values(%{$users}))
-    {
-        # print(Dumper($user));
-        printf("Saving user %s\n", $h_user->{'name'});
-        $h_user->{'time'} = time;
-        my $user;
-        if($user = Ikariam::User->retrieve($h_user->{id}))
-        {
-            foreach my $i (keys(%$h_user)) {
-                # eval($c->$i($h_user->{$i}));
-                $user->set($i => $h_user->{$i});
+            my $user = Ikariam::User->retrieve($h_user->{'id'});
+            if(defined($user)) {
+                foreach (keys(%$h_user)) { $user->set($_ => $h_user->{$_}); }
+            } else {
+                $user = Ikariam::User->insert($h_user);
             }
+            $user->update();
+            saveAlly($h_user->{allyId});
+        }
+    }
+}
+
+sub saveAlly
+{
+    my $allyId = shift;
+    return unless (defined($allyId) && $allyId ne '0');
+
+    # Save for member of ally 
+    my $ally = Ikariam::Ally->retrieve($allyId);
+    if(!defined($ally) || $ally->time le (time - 30*60*1)) {
+        my $h_ally = $::i->viewAlly($allyId);
+        $h_ally->{'time'} = time;
+        if(defined($ally)) {
+            foreach (keys(%$h_ally)) { $ally->set($_ => $h_ally->{$_}); }
         } else {
-            $user = Ikariam::User->insert($h_user);
+            $ally = Ikariam::Ally->insert($h_ally);
         }
-        $user->update();
     }
 }
 
+sub saveIslands {
+    my @islands = @_;
+
+    foreach my $h_island (@islands)
+    {
+        # printf("checking island %d\n", $h_island->{id});
+
+        my $island;
+        if($island = Ikariam::Island->retrieve($h_island->{id})) {
+            foreach my $i (keys(%$h_island)) {
+                $island->set($i => $h_island->{$i});
+            }
+        } else {
+            $island = Ikariam::Island->insert($h_island);
+        }
+
+        # scanning the island
+        # 10 minutes cache.
+        if($island->time le (time - 60*10))
+        {
+            my @cities = $::i->viewIsland($h_island->{id});
+            saveCities($h_island->{id}, @cities);
+
+            $island->set('time', time);
+        }
+        $island->update();
+
+    }
+}
+
+
+# local $SIG{ALRM} = sub { die "timeout\n" };
+# alarm 3;
 our $i = new Ikariam($::server, $::user, $::pass);
 
-my @islands;
 if($#ARGV == 1) {
     $i->login;
-    @islands = $i->viewWorldMap($ARGV[0], $ARGV[1]);
+    my @islands = $i->viewWorldMap($ARGV[0], $ARGV[1]);
+    saveIslands(@islands);
+    $i->logout;
 } elsif($#ARGV == 0) {
     $i->login;
     my $island = $ARGV[0];
-
     my @cities = $i->viewIsland($island);
     saveCities($island, @cities);
     $i->logout;
-    return;
 } elsif($#ARGV == -1) {
     $i->login;
-    @islands = $i->viewHomeMap();
+    my $cities = $i->check;
+    # random
+    foreach my $cityId (keys(%$cities)) {
+# Carp::carp(sprintf("Checking island location [%s:%s]\n", $cities->{$cityId}->{island}->{x},  $cities->{$cityId}->{island}->{y}));
+        my @islands = $i->viewWorldMap( $cities->{$cityId}->{island}->{x},  $cities->{$cityId}->{island}->{y});
+        saveIslands(@islands);
+    }
+    $i->logout;
 } else {
     die("Usage: $0\nUsage: $0 x y\n");
 }
-
-foreach my $h_island (@islands)
-{
-    printf("checking island %d\n", $h_island->{id});
-
-    my $island;
-    if($island = Ikariam::Island->retrieve($h_island->{id})) {
-        foreach my $i (keys(%$h_island)) {
-            $island->set($i => $h_island->{$i});
-        }
-    } else {
-        $island = Ikariam::Island->insert($h_island);
-    }
-
-    # scanning the island
-    if($island->time le (time - 60*60*6))
-    {
-        my @cities = $i->viewIsland($h_island->{id});
-        saveCities($h_island->{id}, @cities);
-
-        $island->set('time', time);
-    }
-    $island->update();
-
-}
-$i->logout;
-# $i->getCityInfo();
--- a/scores.pl	Thu Oct 23 00:47:56 2008 +0800
+++ b/scores.pl	Mon Dec 01 15:32:44 2008 +0800
@@ -19,8 +19,7 @@
 {
     my $users = shift;
 
-    foreach my $user (values(%{$users}))
-    {
+    foreach my $user (values(%{$users})) {
         printf("Saving %s\n", $user->{'name'});
         if(my $c = Ikariam::User->retrieve($user->{id}))
         {
--- a/sheep.pl	Thu Oct 23 00:47:56 2008 +0800
+++ b/sheep.pl	Mon Dec 01 15:32:44 2008 +0800
@@ -4,9 +4,18 @@
 use Data::Dumper;
 
 package main;
+my $mime = Ikariam::User->retrieve ('name' => $::user);
+my $myAlly = undef;
+$myAlly = Ikariam::Ally->retrieve($mime->allyId)
+    if(defined($mime->allyId) && $mime->allyId ne '0');
+
+# only challenge the small victims
+my $army_score_main = ($mime->army_score_main / 3);
+
 my @tradegoodText = qw/NULL 葡萄酒 大理石 水晶 硫磺/;
 
 Ikariam::User->has_many(cities => 'Ikariam::Cities');
+Ikariam::User->has_a(ally => 'Ikariam::Ally');
 Ikariam::User->set_sql(inactivity => qq {
     SELECT user.id 
       FROM user, cities, island
@@ -19,38 +28,17 @@
         AND cities.status = 'i'
     }
 );
-#Ikariam::User->set_sql(sheeps => qq {
-#    SELECT user.id 
-#      FROM user, cities, island
-#      WHERE user.id == cities.user
-#        AND cities.island == island.id
-#        AND island.x <= ?
-#        AND island.x >= ?
-#        AND island.y <= ?
-#        AND island.y >= ?
-#        AND user.trader_score_secondary >= 50000
-#        AND user.army_score_main <= 100
-#    }
-#);
 
 Ikariam::User->set_sql(sheeps => qq {
         SELECT user.id 
           FROM user, cities 
          WHERE user.id == cities.user 
-           AND user.trader_score_secondary >= 200
-           AND user.army_score_main <= 100
+           AND user.trader_score_secondary >= user.army_score_main*3
+           AND user.army_score_main <= $army_score_main
            AND cities.island IN (SELECT island.id FROM island WHERE island.x <= ? AND island.x >= ? AND island.y <= ? AND island.y >= ? )
     }
 );
 
-if($#ARGV != 1) {
-    die("Usage: $0 x y\n");
-}
-my ($x, $y) = @ARGV;
-
-listSheeps(Ikariam::User->search_sheeps(($x + 6), ($x - 6), ($y + 6), ($y - 6)));
-# listSheeps(Ikariam::User->search_inactivity(($x + 6), ($x - 6), ($y + 6), ($y - 6)));
-
 sub listSheeps
 {
     my @sheeps = @_;
@@ -61,19 +49,19 @@
         # avoid duplicate
         next if($sheep->id == $s); $s = $sheep->id;
 
-        # 查聯盟數量
-        my $members = 1;
-        unless ($sheep->allyId == '0') {
-            $members = Ikariam::User->search(allyId => $sheep->allyId)->count();
-        }
-
         foreach my $c ($sheep->cities) {
             my $line = "";
             # Ignore 假期模式
             next if($c->status eq 'v');
 
             unless($c->status eq 'i') {
-                next if($members > 5);
+                # 依照影響力區分
+                unless ($sheep->allyId == '0') {
+                    unless (!defined($sheep->allyId) || $sheep->allyId == 0) {
+                        my $ally = Ikariam::Ally->retrieve($sheep->allyId);
+                        next if($ally->score > $myAlly->score);
+                    }
+                }
             }
 
             my $island = Ikariam::Island->retrieve($c->island);
@@ -81,16 +69,20 @@
             # 所得金錢 = 對方城鎮等級x(對方城鎮等級-1)x對方金錢/10000
             my $capture = $c->citylevel * ($c->citylevel - 1) * $sheep->trader_score_secondary / 10000;
 
-            next if($capture < 100);
+            next if($capture < 200);
 
-            $line = sprintf("%d %s army %d %s/%s(%d),", 
+            # 測試風險評估
+            # $capture = $capture - ($sheep->army_score_main*100);
+                        
+            $line = sprintf("%d %s score %d army %d %s/%s,", 
                 $capture,
-                $c->status, $sheep->army_score_main, $sheep->name, $sheep->ally, $members);
+                $c->status, $sheep->score, $sheep->army_score_main, $sheep->name, $sheep->ally);
 
-            $line .= sprintf("\"%s\" %d [%d,%d] %s http://s2.ikariam.tw/index.php?view=island&id=%d&selectCity=%d\n",
+            $line .= sprintf("\"%s\" %d [%d,%d] %s http://%s/index.php?view=island&id=%d&selectCity=%d\n",
                 $c->cityname, $c->citylevel, 
                 $island->x, $island->y,
                 $tradegoodText[$island->tradegood],
+                $::server,
                 $island->id,
                 $c->cityId
             );
@@ -99,4 +91,12 @@
         }
     }
 }
-# find_or_create
+
+__MAIN__:
+
+if($#ARGV != 1) { die("Usage: $0 x y\n"); }
+my ($x, $y) = @ARGV;
+
+listSheeps(Ikariam::User->search_sheeps(($x + 6), ($x - 6), ($y + 6), ($y - 6)));
+# listSheeps(Ikariam::User->search_inactivity(($x + 6), ($x - 6), ($y + 6), ($y - 6)));
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/usebot.sh	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,85 @@
+#!/bin/bash
+#Auther: billy3321
+#Version: 0.1
+#Interface for Eagle-Eye ikariam bot project
+#for more information please visit
+# https://www.assembla.com/wiki/show/eagle-eye
+
+function seteagleeye() {
+read -p "Enter your server ( s1/s2/s3/s4 ): " SER_NAME
+read -p "Enter your account: " ACC_NAME
+read -p "Enter your password: " PW_NAME
+echo -e "package main;\n\n\$::server = \"${SER_NAME}.ikariam.tw\";\n\$::user = \"${ACC_NAME}\";\n\$::pass = \"${PW_NAME}\";\n\n1;" > ${HOME}/.eagleeye.pm
+}
+
+function chihchunloop() {
+read -p "Please enter the coordinate you want to fight(ex: 39:49)" SITE
+SITE_X=$(echo $SITE | cut -d : -f 1)
+SITE_Y=$(echo $SITE | cut -d : -f 2)
+while : ; do 
+    perl agent.pl
+     
+    perl scan.pl
+ 
+    let RAN_X=$RANDOM%100
+    let RAN_Y=$RANDOM%100
+    perl scan.pl $RAN_X $RAN_Y
+      
+    perl warfare.pl $SITE_X $SITE_Y
+done
+}
+
+echo "Checking for Database..."
+if test -f ikariam.sqlite;then
+	echo "Find Database, continue..."
+else
+	echo "setting up database...."
+	cat ikariam.sql | sqlite3 ikariam.sqlite
+	echo "Database Setting finished. Continue Program..."
+fi
+
+echo "Checking for account information file..."
+if test -f ${HOME}/.eagleeye.pm ; then
+	echo "Find your account information file. Continue Program..."
+else
+	echo "Can't find your account information file. We will creat one"
+	seteagleeye
+	
+fi
+
+while true
+do
+	echo -e "Welcome to use eagle-eye ikariam bot. Please select the action you want to do.\n 1. Reset the account information file.\n 2. Reset the databse.\n 3. Scan around your island.\n 4. Run the bot.\n 5. List the sheeps(Please scan first).\n 6. Chihchun's loop, automatic scan and attack sheep.\n 0. Exit Program."
+	
+	read -p "What do you want to do now? Please enter the number:" ACT
+	case $ACT in
+	 "1")
+	    seteagleeye
+	    ;;
+	 "2")
+	    rm ikariam.sqlite
+	    cat ikariam.sql | sqlite3 ikariam.sqlite
+	    ;;
+	 "3")
+	    perl scan.pl
+	    ;;
+	 "4")
+	    perl agent.pl
+	    ;;
+	 "5")
+	    perl sheep.pl
+	    ;;
+	 "6")
+	    chihchunloop
+	    ;;
+	 "0")
+	    break
+	    ;;
+	 *)
+	    echo "Please enter a number."
+	    ;;
+	 esac
+done
+
+
+#END
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/warfare.pl	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,169 @@
+#!/usr/bin/perl
+use strict;
+use Ikariam;
+use Data::Dumper;
+use Decision::ParseTree q{ParseTree};
+use YAML qw/LoadFile Dump DumpFile/;
+
+package Ikariam::Warfare::Rules;
+use strict;
+use Data::Dumper;
+
+Ikariam::User->has_many(cities => 'Ikariam::Cities');
+Ikariam::Cities->has_a(user => 'Ikariam::User');
+Ikariam::Cities->has_a(island => 'Ikariam::Island');
+Ikariam::Cities->set_sql(sheeps => qq {
+        SELECT cities.cityId
+          FROM user, cities 
+         WHERE user.id = cities.user 
+           AND user.army_score_main = 0
+           AND cities.island IN (SELECT island.id FROM island WHERE island.x <= ? AND island.x >= ? AND island.y <= ? AND island.y >= ? )
+         ORDER BY cities.citylevel * (cities.citylevel - 1) * user.trader_score_secondary / 10000 DESC
+    }
+);
+
+Ikariam::Report->set_sql(victim => qq {
+        SELECT report.id
+          FROM report
+         WHERE report.city = ?
+           AND report.date >= ?
+    } 
+);
+
+sub new {
+    my ( $class, $i ) = @_;
+    my $self = {
+        ikariam => $i,
+    };
+    return bless $self, $class;
+}
+
+sub is_attacked {
+    my ($self, $city) = @_;
+    return ($self->{'ikariam'}->{'military'}->{attacks} > 0 ) ? 1 : 0;
+}
+
+sub is_actionPoint_enough {
+    my ($self, $city) = @_;
+    return ($city->{actionPoints} > ($city->{maxActionPoints}/2) ) ? 1 : 0;
+}
+
+sub is_transporters_available {
+    my ($self, $city) = @_;
+    # we keep 10 transporters for prize
+    return ($city->{transporters}->{avail} > 10) ? 1 : 0;
+}
+
+sub is_port_available {
+    my ($self, $city) = @_;
+    foreach(1..2) {
+        return 1 if($city->{locations}[$_] eq 'port');
+    }
+    return 0;
+}
+
+sub is_army_available {
+    my ($self, $city) = @_;
+    return ($city->{army}->{Swordsman} >= 2 ) ? 1 : 0;
+}
+
+sub locateVictim {
+    my ($self, $city, $x, $y, $tradegood) = @_;
+
+    my $user = Ikariam::User->retrieve('name' => $::user);
+
+    my @cities = Ikariam::Cities->search_sheeps(($x + 6), ($x - 6), ($y + 6), ($y - 6));
+CITY: foreach my $city (@cities) {
+        my $sheep = $city->user;
+        my $island = $city->island;
+
+        # TODO update sheep and island information.
+
+        # we don't fight friends.
+        # FIXME: This is very dirty for accessing to $::i directly.
+        foreach (keys(%{$::i->{friends}})) {
+            next CITY if ($sheep->id == $_);
+        }
+
+        # we don't fight with members in same ally.
+        next if($sheep->allyId == $user->allyId);
+
+        # we fight for island which ownes differnet trade goods.
+        next if($island->tradegood == $tradegood);
+        # Ignore the user in vacation which we can not attack.
+        next if($city->status eq 'v');
+        unless ($city->status eq 'i') {
+            unless (!defined($sheep->allyId) || $sheep->allyId == 0) {
+                my $ally = Ikariam::Ally->retrieve($user->allyId);
+                my $targetAlly = Ikariam::Ally->retrieve($sheep->allyId);
+                # We don't want to piss off the big team.
+                next if($targetAlly->score > $ally->score);
+                # next if(defined($ally) && $ally->members > 10);
+            }
+        }
+
+        # check if we over-attacked
+        my $c = Ikariam::Report->search_victim($city->cityId, time - 24*60*60 - 7*60*60)->count();
+        # check the current plunders
+        foreach (@{$self->{ikariam}->{'military'}->{'homeland'}}) {
+            # counting for both leaving and coming back.
+            $c++ if($_->{to} == $city->cityId || $_->{from} == $city->cityId);
+        }
+        # we attack one city maximum 4 times a day.
+        next if($c >= 4);
+
+        my $capture = $city->citylevel * ($city->citylevel - 1) * $sheep->trader_score_secondary / 10000;
+        next if($capture < 1500);
+
+        # {
+            my $line = sprintf("%d %s [%d,%d] %s (%d) %s Attacked %d", 
+                $capture, $city->status, 
+                $island->x, $island->y,
+                $city->cityname, $city->cityId,
+                $sheep->name, $c);
+            
+            printf("%s\n", $line);
+        # }
+        return $city->cityId;
+    }
+    return undef;
+}
+
+sub engagement {
+    my ($self, $cityId, $x, $y, $tradegood) = @_;
+    my $victim = $self->locateVictim($cityId, $x, $y, $tradegood);
+    if(defined($victim)) {
+        $self->{ikariam}->changeCity($cityId);
+        $self->{ikariam}->plunderCity($victim);
+    }
+}
+
+1;
+
+package main;
+
+our $i = new Ikariam($::server, $::user, $::pass);
+our $rules = Ikariam::Warfare::Rules->new($i);
+
+$i->login;
+my $cities = $i->check;
+$i->checkMilitaryAdvisorCombatReports();
+$i->checkFriends();
+
+foreach my $cityId (keys(%$cities)) {
+    print Dump($i->{'military'});
+    # build and upgrade for cities
+    my $tree  = LoadFile('warfare.yaml');
+    $cities->{$cityId}->{parse_path} = [];
+    $cities->{$cityId}->{parse_answer} = undef;
+    if(ParseTree($tree, $rules, $cities->{$cityId}) eq 'engagement') {
+        my $island = Ikariam::Island->retrieve($cities->{$cityId}->{island}->{id});
+        $rules->engagement($cityId, $island->{x}, $island->{y}, $island->tradegood);
+        sleep(30);
+        $i->checkMilitaryAdvisorMilitaryMovements();
+    }
+    print Dump($cities->{$cityId}->{parse_path});
+}
+DumpFile("warfare-dump.yaml", $cities);
+
+$i->logout;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/warfare.yaml	Mon Dec 01 15:32:44 2008 +0800
@@ -0,0 +1,11 @@
+---
+- is_attacked:
+    0:
+       - is_actionPoint_enough:
+          1:
+             - is_transporters_available:
+                1:
+                   - is_port_available:
+                      1:
+                         - is_army_available:
+                            1: engagement