What have you found for these years?

2009-08-24

oop without state with fp (3)

呃 well, 與其說在做 oop with fp,
不如說完全在做 spellbook 的 model 了 @@

從上篇到現在,中間解決了不少問題,
不過沒找到空閒把他們寫下來...
總之現在已經做成完全的 type safe 了:

sealed abstract class Property{
type This <: Property
val pt: Int
val create: Int => This

def +(p: This): This = create(pt + p.pt)
def -(p: This): This = create(pt - p.pt)
}

因此 Health 一定要跟 Health 相加減,其他的亦然。
也不允許 Int 上的操作了,多寫個 constructor 又不難。
so:

Footman.state.agility - 10 // type error
Footman.state.agility - Agility(10) // Agility(5)
Footman.state.agility - Health(10) // type error

大概是這樣。然後是抓了 scala 2.8 的 nightly build,
因為 default arguments + named arguments,
可以讓我很多地方少打非常非常非常多。
之前建立 property 會需要列舉,現在則是:

def +(property: Property): State = property match{
case h: Health => create( health = _1 + h)
case m: Mana => create( mana = _2 + m)
case e: Energy => create( energy = _3 + e)
case v: Vigor => create( vigor = _4 + v)
case s: Strength => create( strength = _5 + s)
case c: Constitution => create(constitution = _6 + c)
case i: Imagination => create( imagination = _7 + i)
case w: Will => create( will = _8 + w)
case a: Agility => create( agility = _9 + a)

而 scala 2.8 的 copy method, 我用起來怪怪的,暫時不能用。
沒差,反正是多出來的,等到正式版出來再看看。
原本上面那段,沒有 named arguements, 超級長...

然後關於移動的部份,變成這樣:

val river: Terrain = River() + Energy(10)
river.stay_here(Footman)

stay_here 回傳是 (Terrain, Creature)
因為這邊 river 會被吸走 Energy, 而 Footman 則會得到 energy.
兩個狀態都改變了,所以兩個新狀態必須被回傳。

比較奇怪的是,這邊應該是 Footman.stay_here(river)
才像正常主動被動關係吧?問題在於,所有的生物都是
Creature 的 instance, 而 Terrain 卻是 abstract 的,
底下有 Road, Forest, Lava, River, Plains 五種 subclass.
如果我寫成 Footman.stay_here, 那我會需要 pattern matching.
但是如果反過來,因為 dynamic binding, 直接就能叫到那個地形的
特殊處理 method. 這樣一來,程式會變得比較乾淨簡潔。

其實問題在 stay_here 這個名字取得不好。
應該用類似 put_a_creature_here 之類的...
但這麼長的爛名字我才不用咧 XD
所以... 徵求好名字 XD 就先這樣了。

*

不過以上都是前幾天就寫好的。
剛才寫好的部份是行動的部份,應該比上面的重要。
現在大概會是這樣用:

val from = Block(river, Footman)
val to = Block(river, Footman)
println(MeleeAttack().activate(from, to))

activate 的回傳 type 是 (Block, List[Block])
前者是新的 from block, 後者是所有受影響的 block.
在這邊,MeleeAttack 當然只影響到 to.
以後有範圍魔法時才會有一次回傳一堆 block 的狀況。

這邊的考量是,在 GUI 中,我們會保有 user 選擇的 Ability,
因此讓 activate 成為 Ability 的 method 應該合理。
本來是想做成像是 Footman.abilities.head.activate(target)
這樣看起來很順,但是會碰到很多問題:

1. 我們怎麼決定抽出哪個 ability?
2. 失去 block 資訊,也就是說 Footman 的所在地資訊遺失了

也就是說 block 一定要傳進去,不然就是要讓 ability 認識
block. 但這樣做絕對會搞到很複雜。因此傳 block 進去比較簡單。
所以 GUI 中的選單,應該是記錄所有 abilities, 比方說:

Menu(Footman.abilities)

然後 GUI 會有 callback 得知是哪個 ability 被按下,
接著就能 ability.activate(from, to)
from 就是現在正在控制的 creature, 一定有記錄。
to 就是等會選擇的目標了。

Ability#activate 的定義則很單純,不是 abstract 的!
由 subclass 定義三種 method, 分別是 consume,
select, 和 apply.

def activate(from: Block, to: Block): (Block, List[Block]) = {
(consume(from), select(to).map(apply(from, _)))
}

consume 定義消耗,select 定義目標範圍,type 是:
Block => List[Block]
比方說周遭一格之類的,這個 List 的 size 就是 7.
蜂窩的六格加上原本的那一格。

接著用 apply 去做 map, apply 的 type 是:
Block => Block => Block
第一個 Block 是 from, 也就是操作的那一格,
第二格就是目標的那一格,剛剛被 select 出來的其中一格。

因此 subclass, 也就是 MeleeAttack, 只要定義這三個即可。
其中 select 不用 override, 用原本的定義即可。
Ability 替這三個 method 都提供了預設行為,
consume 當然就是什麼也不做,select 則是只選那一格,
apply 也是什麼都不做。因此 MeleeAttack 不用改寫 select.

consume 就單純消耗 Vigor, 也就是行動點數。
apply 則是看 from.creature.state.strength.pt 減掉
to.creature.state.constitution.pt, 這樣算出來叫 damage.
接著就是 to.creature - Health(damage)

所以傷害就是單純 strength - constitution
這邊只是實驗可行性,以後要改寫的部份還很多。
包括屬性的影響... 還沒寫上去。地形的影響也還沒有。
現在地形都是很單純忽略,直接把丟進來的原封不動丟回去。

*

另外我碰到一個難題,就是 circular reference...
map 記錄 block, 然後 block 連回 map,
這在 imperative 的世界裡,再單純也不過了。
但是在 no state 的狀況下,我沒辦法動態產生這種結構 @@

因此我現在 block 是沒有記錄 map 的...
這個問題在於,要選擇周圍一格的動作,
勢必得變成某種 linked list 的結構。
但地圖這樣建應該會麻煩死,效能也很爛吧..?

這個問題先丟到後面慢慢想 @@
真的沒辦法的話,scala 也不是沒有 var 可以用...(逃)
但當然這是下下策,沒辦法時才考慮吧...

另一方面,在這種四處是 state 的遊戲 model 裡,
要這樣寫感覺挑戰性真的是不小啊 XD
所有會被改變的東西,都要做成 tuple 回傳回去...

於是真的要進行遊戲流程,可能很適合包成 monad.
不知道 scala 裡有沒有辦法做漂亮的 monad...?

==
其實我已經不敢想像要怎麼翻譯成 haskell 了... XD
這最後要寫結論之類的,再來傷腦筋吧...

還有就是在想要不要丟到 github 上?
雖然很沒有內容,就印一些資料而已...
但複雜程度已經非常高了 @@
丟上去的話要展示或什麼的也比較方便。

0 retries:

Post a Comment

Note: Only a member of this blog may post a comment.



All texts are licensed under CC Attribution 3.0