2011年2月10日 星期四

Acceptance Tests

Extreme programming uses testing two ways: Customers develop acceptance tests that determine the overall behavior of the system, and Programmers create unit tests while programming.

As a Customer, you'll create tests for each story you request. You'll measure the progress in developing the system by running these tests.

目標: 偷竊物理模擬小遊戲.

物理模擬部分,已經抽出「合適」的 interfaces,implementor 採用 Box2D,為了「證明」Box2D 滿足遊戲 (customer) 需求,自然要有幾個簡單的小測試。這種測試就稱為 acceptance tests。

第一個測試就是自由落體 (freefall):
TEST(Box2d, Freefall) {
Service* service = new Box2dService();

// Construct a world, which will hold and simulate the rigid bodies.
WorldConfig world_config;
world_config.gravity.set(0.0f, -10.0f);
World* world = service->createWorld(world_config);

// Config ground body.
BodyConfig ground_config;
ground_config.position.set(0.0f, -10.0f);
Body* ground = world->createBody(ground_config);

// Config ground body fixture.
PolygonShapeConfig ground_shape_config;
ground_shape_config.setAsBox(50.0f, 10.0f);

FixtureConfig ground_fixture_config;
ground_fixture_config.shape_config = &ground_shape_config;
ground_fixture_config.density = 0.0f;
ground->createFixture(ground_fixture_config);

// Config dynamic body.
BodyConfig dynamic_config;
dynamic_config.type = BodyConfig::Dynamic;
dynamic_config.position.set(0.0f, 4.0f);
Body* dynamic = world->createBody(dynamic_config);

// Config dynamic body fixture.
PolygonShapeConfig dynamic_shape_config;
dynamic_shape_config.setAsBox(1.0f, 1.0f);

FixtureConfig dynamic_fixture_config;
dynamic_fixture_config.shape_config = &dynamic_shape_config;
dynamic_fixture_config.density = 1.0f;
dynamic_fixture_config.friction = 0.3f;
dynamic->createFixture(dynamic_fixture_config);

// Simulate.
const float time_step = 1.0f/60.0f;
const int steps = 60;
const float expected[] = {
3.997222f, 3.991667f, 3.983333f, 3.972222f, 3.958333f, 3.941667f,
3.922222f, 3.900000f, 3.875000f, 3.847222f, 3.816667f, 3.783333f,
3.747222f, 3.708333f, 3.666667f, 3.622222f, 3.575000f, 3.525000f,
3.472222f, 3.416667f, 3.358333f, 3.297222f, 3.233333f, 3.166667f,
3.097222f, 3.025000f, 2.950000f, 2.872222f, 2.791667f, 2.708333f,
2.622222f, 2.533333f, 2.441667f, 2.347222f, 2.250000f, 2.150000f,
2.047222f, 1.941667f, 1.833333f, 1.722222f, 1.608334f, 1.491667f,
1.372223f, 1.250000f, 1.125000f, 1.014582f, 1.014651f, 1.014708f,
1.014756f, 1.014796f, 1.014830f, 1.014858f, 1.014881f, 1.014900f,
1.014917f, 1.014930f, 1.014942f, 1.014951f, 1.014959f, 1.014966f
};
EXPECT_EQ(steps, arraysize(expected));

for (int i = 0; i < steps; ++i) {
world->simulate(time_step);
EXPECT_TRUE(fabs(dynamic->position().x()) < 0.001f);
EXPECT_FLOAT_EQ(expected[i], dynamic->position().y());
EXPECT_TRUE(fabs(dynamic->angle()) < 0.001f);
}

// Clean up.
delete world;
delete service;
}
程式碼有點長,偷自 Box2d's HelloWorld,哈哈,天下文章一般抄。若要比較 float/double 大小,EXPECT_FLOAT_EQ() macro 是一個很好的選擇。總之,此 acceptance test 沒過,我們必須讓他過。(只貼有意思的部分)
// box2d_world.cc
Body* Box2dWorld::createBody(const BodyConfig& config) {
b2BodyDef config_impl;

config_impl.position.Set(config.position.x(), config.position.y());
config_impl.angle = config.angle;
config_impl.linearVelocity.Set(config.linear_velocity.x(),
config.linear_velocity.y());
config_impl.angularVelocity = config.angular_velocity;
config_impl.linearDamping = config.linear_damping;
config_impl.angularDamping = config.angular_damping;
if (config.type == BodyConfig::Static) {
config_impl.type = b2_staticBody;
} else if (config.type == BodyConfig::Dynamic) {
config_impl.type = b2_dynamicBody;
} else if (config.type == BodyConfig::Kinematic) {
config_impl.type = b2_kinematicBody;
}
config_impl.active = config.is_active;

return new Box2dBody(itself_->CreateBody(&config_impl));
}

// box2d_body.cc
void Box2dBody::createFixture(const FixtureConfig& config) {
if (config.shape_config->type == ShapeConfig::Polygon) {
PolygonShapeConfig* shape
= dynamic_cast< PolygonShapeConfig* >(config.shape_config);

b2PolygonShape shape_impl;
shape_impl.m_vertexCount = static_cast(shape->vertices.size());
for (int i = 0; i != shape_impl.m_vertexCount; ++i) {
shape_impl.m_vertices[i].Set(shape->vertices[i].x(),
shape->vertices[i].y());
shape_impl.m_normals[i].Set(shape->normals[i].x(),
shape->normals[i].y());
}
shape_impl.m_centroid.Set(shape->centroid.x(), shape->centroid.y());

b2FixtureDef config_impl;
config_impl.shape = &shape_impl;
config_impl.friction = config.friction;
config_impl.restitution = config.restitution;
config_impl.density = config.density;

itself_->CreateFixture(&config_impl);
}
}

偷加一個 method 給 PolygonShapeConfig:
void PolygonShapeConfig::setAsBox(float half_x, float half_y) {
vertices.push_back(math::Vector2(-half_x, -half_y));
vertices.push_back(math::Vector2( half_x, -half_y));
vertices.push_back(math::Vector2( half_x, half_y));
vertices.push_back(math::Vector2(-half_x, half_y));

normals.push_back(math::Vector2( 0.0f, -1.0f));
normals.push_back(math::Vector2( 1.0f, 0.0f));
normals.push_back(math::Vector2( 0.0f, 1.0f));
normals.push_back(math::Vector2(-1.0f, 0.0f));

centroid.setZero();
}
跑一跑居然過了,太神奇了傑克。這就是一個簡單的 acceptance test,謝謝大家收看。

2011年2月9日 星期三

Bernanke Says Unemployment Will ‘Remain Elevated’

Link: Bloomberg

記住,這只是 Bernanke 的意見,市場「某個」聲音,失業率將來怎麼走,只有上帝知道。「失業率」不重要,重要的是「大眾對失業率的反應」,心理學造就百分之九十的行情。

失業率是什麼?很簡單,失業率是「失業人口佔勞動力的比率」,大眾怎麼反應,只有市場會告訴你答案,政治裡也有答案,不要談政治,不要談市場,上帝造人,為什麼造兩個耳朵、兩隻眼睛,卻只造一個嘴巴?Remain Elevated?

最後祝大家兔年躺著爽爽賺,謝謝大家。

Development Driven by Tests

哲學: Development is driven by tests. You test first, then code. Until all the tests run, you aren't done. When all the tests run, and you can't think of any more tests that would break, you are done adding functionality.

例子: (Use case) 儲存/讀取遊戲設定。

先寫 test: (使用 Google C++ Testing Framework)
TEST(WinFileRepository, SetValue) {
const std::string name("options");
// Set
{
Repository* repository = new WinFileRepository(name);
repository->setValue("reset_level_button", "single_tap");
repository->setValue("offset", "off");
delete repository;
}

// Get
{
Repository* repository = new WinFileRepository(name);
EXPECT_TRUE(repository->value("reset_level_button") == "single_tap");
EXPECT_TRUE(repository->value("offset") == "off");
delete repository;
}
}

Compile failed. 用最簡單的方式實作,目的只是為了 pass compiling:
void WinFileRepository::setValue(...) {
}

std::string WinFileRepository::value(...) const {
return default_value;
}

Compile 過了,但是 test 不過。那就實作正確的程式碼吧:(略過細節)
// win_file_repository.h
class WinFileRepository : public Repository {
public:
WinFileRepository(...);
~WinFileRepository();

void setValue(...);
std::string value(...) const;

private:
void read();
void write();

std::string file_name_;
std::map map_;
std::string separator_;
std::string delimiter_;
};

// win_file_repository.cc
namespace {

bool ReadFileToString(...) { ... }
int WriteFile(...) { ... }

} // namespace

WinFileRepository::WinFileRepository(...)
: file_name_(name),
separator_(separator),
delimiter_(delimiter) {
read();
}

WinFileRepository::~WinFileRepository() {
write();
}

void WinFileRepository::setValue(...) {
map_[key] = value;
}

std::string WinFileRepository::value(...) const {
std::map::const_iterator it = map_.find(key);
if (it != map_.end()) {
return it->second;
} else {
return default_value;
}
}

void WinFileRepository::sync() {
write();
}

void WinFileRepository::read() {
std::string contents;
ReadFileToString(file_name_, &contents);

size_t prev_pos = 0;
for (size_t pos = contents.find(delimiter_, prev_pos);
pos != std::string::npos;
pos = contents.find(delimiter_, prev_pos)) {
const size_t sep_pos = contents.find(separator_, prev_pos);

map_[contents.substr(prev_pos, sep_pos - prev_pos)]
= contents.substr(sep_pos + 1, pos - sep_pos - 1);

pos++;
prev_pos = pos;
}
}

void WinFileRepository::write() {
std::string contents;
std::map::iterator it;
for (it = map_.begin(); it != map_.end(); it++) {
contents.append(it->first);
contents.append(separator_);
contents.append(it->second);
contents.append(delimiter_);
}

WriteFile(file_name_, contents.c_str(), static_cast(contents.size()));
}
最後真的 pass test 了,感動。以上大量參考 Chromium projects, Qt4...抄才是王道呀!當然實作很醜,要漂亮就得用 protocol buffer 或是 boost::serialization,但個人真的太懶了,能夠 pass tests 就夠了。

2011年2月8日 星期二

On Forecasts

Zurich axiom: Human behavior cannot be predicted. Distrust anyone who claims to know the future, however dimly.

Nobody has the foggiest notion of what will happen in the future. Nobody. Never lose sight of the possibility you have made a bad bet.

個人解讀:只有帳戶裡的錢是真的,沒有對帳單,一切都是假的。預測正確不等於操作正確,更不等於賺錢。做對的事,做錯也沒關係,承認錯誤停損就好啦。預測是上帝的事,千萬不要妄想當上帝。

2011年2月5日 星期六

State Patterns in Practice (C++)

Recently I posted a workflow here. I want to express the workflow in terms of state patterns. There are only two steps. Step 1: Prepare the framework.

// state/state.h
#ifndef STATE_STATE_H_
#define STATE_STATE_H_
#pragma once

namespace state {

class State {
public:
virtual ~State() {}

virtual void update() = 0;
virtual void draw() const = 0;
virtual void onEnter() {}
virtual void onLeave() {}
};

} // namespace state

#endif // STATE_STATE_H_

// state/state_factory.h
#ifndef STATE_STATE_FACTORY_H_
#define STATE_STATE_FACTORY_H_
#pragma once

#include <string>

namespace state {

class State;

class StateFactory {
public:
virtual ~StateFactory() {}
virtual State* createState(const std::string& name) = 0;
};

} // namespace state

#endif // STATE_STATE_FACTORY_H_

// state/state_manager.h
#ifndef STATE_STATE_MANAGER_H_
#define STATE_STATE_MANAGER_H_
#pragma once

namespace state {

class State;

class StateManager {
public:
StateManager();
virtual ~StateManager() {}

inline State* current_state() { return current_state_; }
virtual void switchToState(State* state);

private:
State* current_state_;
};

} // namespace state

#endif // STATE_STATE_MANAGER_H_

// state/state_manager.cc
#include "state_manager.h"
#include "state.h"
#include "base/basictypes.h"

namespace state {

StateManager::StateManager()
: current_state_(NULL) {
}

void StateManager::switchToState(State* state) {
if (!!current_state_) {
current_state_->onLeave();
}

if (!!state) {
current_state_ = state;
current_state_->onEnter();
}
}

} // namespace state


Step 2: Combine them. I will not post all details but post important files instead.
// app_state_factory.h (not completed)
#include "state/state_factory.h"

class AppController;

class AppStateFactory : public state::StateFactory {
public:
explicit AppStateFactory(AppController* controller);
virtual state::State* createState(const std::string& name);

private:
AppController* controller_;
};

// app_controller.h
namespace state {
class State;
}

class AppStateManager;
class AppStateFactory;

class AppController {
public:
AppController();
~AppController();

void init();
void deinit();

void update();
void draw() const;

void switchToState(const std::string& name);

private:
void initAllStates();

private:
AppStateManager* state_manager_;
AppStateFactory* state_factory_;
std::map<std::string, state::State*> states_;
};

// app_state_manager.h
#include "state/state_manager.h"

class AppStateManager : public state::StateManager {
public:
AppStateManager();
virtual ~AppStateManager();
};

// state/initial_state.h
#include "state/state.h"

class AppController;

class InitialState : public state::State {
public:
explicit InitialState(AppController* controller);
virtual ~InitialState();

virtual void update();
virtual void draw() const;
virtual void onEnter();
virtual void onLeave();

private:
AppController* controller_;
};

// state/initial_state.cc
#include "initial_state.h"
#include "../app_controller.h"

InitialState::InitialState(AppController* controller)
: controller_(controller) {
}

InitialState::~InitialState() {
}

void InitialState::update() {
}

void InitialState::draw() const {
}

void InitialState::onEnter() {
// TODO: Load necessary resources.
if (!!controller_) {
controller_->switchToState("idle");
}
}

void InitialState::onLeave() {
}

Keep working!

2011年2月4日 星期五

過年加班的彭淮南

Source: 彭淮南 過年要加班

加班名單等於彭淮南信任名單。

此外,在 2011.01.31,彭淮南表示:「會密切注意物價問題,並透露央行對今年核心消費者物價指數(CPI)的估計約在1%左右。」央行估計今年CPI最高應該接近2%。彭淮南表示:「央行更在意的是不含蔬果和油價的核心CPI,今年核心CPI的估計約落在1%附近,相較去年的0.44%高出不少。」

繼續聽央行聲音,留意後續利率變化。

Time in C++ class

Source: [chrome] / trunk / src / base / time.h.

Requirement: get the current time, perform simple arithmetic.

To achieve this goal, we do the following steps:

1. In TimeDelta class, keep the following important methods: converts units of time to TimeDeltas, ToInternalValue(), returns the time delta in some unit, computations with other deltas, comparison operators.

2. In Time class, keep the following important methods: Now(), compute the difference between two times, return a new time modified by some delta, comparison operators.

3. In TimeTicks class, keep the following important methods: Now(), compute the difference between two times, modify by some time delta, return a new TimeTicks modified by some delta, comparison operators.

4. Add base/basictypes.h, base/port.h, and build/build_config.h first.

5. Add base/time.cc and platform-specified implementation (For example, we need base/time_win.cc under the Windows platform). Remove unnecessary included header files and implementations. Remove all debugging and logging codes.

Done! Time class is very important because many games need the concept of time. Keep stealing!