游戏开发日记-战斗核心

引言

正如探险日志名字所表达的那样,该游戏的核心模块是其日志生成系统。而日志生成系统则分为探索和战斗两大块。我的计划便是从战斗模块开始开发。这次开发的是战斗系统的核心,核心开发完后其余的法术系统、策略系统等会一个一个开发并挂载到核心之上。

系统设计

在战斗核心部分,我并不打算模仿探险日志自身回合机制,而是打算模仿Tome4,采用行动点系统。系统会依次扫描每个生物,首先分配点数,然后由该生物进行行动并消耗点数,若有剩余可以继续行动,直到行动点为0或负数为止,然后开始调度下一个生物。另外,如果行动点负值太大,也可能直接跳过该轮。
这种机制的优点在于可以较为简单地实现一轮多动和多轮施法,并且相较传统即时制,对cpu的消耗极少。

模块

战斗核心主要包括入口函数battle Function,时间队列TimeQueue Class,生物Creature class以及其他辅助模块

battle Function

这个函数是战斗系统的入口函数,由于战斗系统最终会挂载在探索系统之下,所以该函数负责的是接受探索系统传来的初始信息,生成战斗报告并返回给上级。由于目前其他部分都还没有设计所以该函数目前只负责初始化一个时间队列,然后调用时间队列生成战斗数据,最后将结果返回。

TimeQueue Class

一开始我只是打算把TimeQueue做成一个类型行动序列的东西。但是后来发现把行动序列单独做成一个小模块就目前而言意义不大,因而最终就做成了一个行动调度器。
该队列的主要入口为run方法。run方法会首先检测是否有一方已经全部被击败,如果没有则判断当前生物是否有行动能力。如果有行动能力则会指令该生物进行行动,最后根据返回的结果生成动作记录并做死亡检测。否则将焦点移动到下一个生物并分配行动点。

Creature Class

生物类代表的是战斗中的所有生物,核心方法为action。action主要是根据战斗局面做出决策选择行动,并将行动结果返回上级,然而决策系统还没开始做,所以目前action是直接调用attack进行攻击

游戏开发日记-引言

一直以来我都希望能找一个开源的游戏项目参与进去。但是成熟的项目过于复杂,别说参与就连读懂都很难,而一些简洁的独立游戏有没有哪个愿意开源的,所以这个想法当现在位置仍然还只是愿望。

而这次打算开始动手的原因是最近玩儿的《探险日志2》。这是一个很赞的idel game,框架设计很不错,非常简洁却又极具改造的潜力。所以我打算干脆自己模仿《探险日志2》也来写一个游戏。

以下是项目地址:https://github.com/woodensail/project1

解决部分jquery插件渲染时删除原标签带来的事件失效和jquery对象丢失

jQuery插件在渲染控件的过程中进程会需要用新的标签来代替初始标签,有写插件会将原标签隐藏同时将事件和函数都绑定在原标签上来保证渲染之前的jquery对象依然有效 ,并且绑定的事件不丢失。
但是,有些插件则会选择将原标标签删除后,把id等必要信息复制入新标签中,在这个过程中,所有指向原标签的jquery对象会全部失效,同时如果插件没有进行处理的话原标签的class和绑定的事件也会丢失。所以需要进行一定的处理。

处理分为两个函数,一个函数在渲染前提取数据,另一个在渲染后注入数据。调用方式如下:

[javascript]
var _data = getData(jq);
jq.** // 此处调用插件的渲染语句
setData(_data);
}
[/javascript]

getData返回一个obj包含了原始标签的包装对象,事件和class,如果有需要还可以继续添加。
setData第一句是用原始的选择器初始化一个jQuery对象获得新标签,然后用新jQuery对象中的dom替换老的,从而保证所有指向老jQuery对象的变量都会受到影响。注意,此处不能直接data.jq=$(data.jq.selector),这样实际上是改变了data.jq的指向,原来的类数组并没有得到改变。
setData第二三句分别将之前提取出的事件和classes注入新标签中。
[javascript]
function getData(jq) {
return {
jq : jq,
events : $.data(jq[0], ‘events’),
classes:
.filter(jq.attr(‘class’).split(‘ ‘), function (v) {
return _.startsWith(v,’dhc-‘);
}).join(‘ ‘)
};
}

function setData(data) {
data.jq[0] = $(data.jq.selector)[0];
$.extend($._data(data.jq[0], ‘events’), data.events);
data.jq.addClass(data.classes);
}
[/javascript]

JVM的编译时多态与类型擦除

众所周知,java代码在编译过程中会进行类型擦除,类型擦除后泛型信息会丢失。可是,为什么在反射中还可以通过ParameterizedType的getActualTypeArguments方法来获得泛型信息呢。

首先,按照网上的说法,下面这两句所生成的字节码应该是一样的。但事实上,可以发现,他们所生成的字节码是不同的,区别就在Signature和LocalVariableTable中,这里标明了泛型的实际类型。

[java]
public static void print(Set<Integer> c) { } // LocalVariableTable中为Ljava/util/Set<Ljava/lang/Integer;>
public static void print(Set<String> c) { } // LocalVariableTable中为Ljava/util/Set<Ljava/lang/String;>
[/java]

看上去,泛型信息还在,可是为什么这两句放在一起会报错呢?

因为java采用的是编译时多态。java代码在编译过程中会尝试匹配所有同名方法,并且找到唯一符合条件的方法,然后将其签名写入字节码中,通过其签名来调用该方法。因此java中方法名可以相同,但是调用该方法的签名决不能相同
在此例中上面两个方法生成的字节码虽然不同,但是在调用时所用的签名都是”print:(Ljava/util/List;)V”,因而无法区分这两个方法,所以不能多态。
正是因为编译时多态这种编译时分析确定签名,运行时根据签名直接调用的方式。导致下面这两句是完全可以执行的,原因就是虽然实参与形参不一致,但是实参与形参在忽略泛型后是一样的。
[java]
// 注意,这仅仅是个例子,由于编译器的检查功能所以这两句是无法编译通过的。因此需要通过反射的方式来实现。
// 当然,最简单的方式是将这两句分到两个类中用一些trick将两个class分别编译后直接运行,你可以发现代码是可以正常执行的,虽然直接编译它一定会报错。
print(new Set<Integer>());
public static void print(Set<String> c) { }
[/java]

由此可见,java中泛型擦除的确是存在的,运行时的基本可以认为泛型已经被完全擦除。但是,为什么ParameterizedType还是可以取得泛型信息呢?
答案是字节码。要知道,反射与JVM正常的运行是不同的,反射可以直接分析字节码,而字节码中有该类的签名以及变量的签名,从而可以分析出类或变量的泛型信息。

最后,通过查看字节码的反汇编结果可知以下情况中的泛型是可以通过反射获取的。
1:函数返回值中的泛型
2:函数参数中的泛型
3:类的field中的泛型
4:函数中局部变量的泛型「存于LocalVariableTypeTable中,仅用于调试,发布模式下不存在」

数据分析S110:网站链接分析(一)

这是网站链接分析的第一期,这一次的首要目的是从excle文件中提取出页面及页面之间的链接,其次就是完成初步的信息提取,即找出指定页面的链入链出情况,以及分析网站的链入链出。
[python]
author = ‘sail’
db_filename = r’resource/link.db’
schema_filename = r’link.sql’
import sqlite3

def read_data():
import os
import pickle
from multiprocessing import Pool

# 建立数据库及数据库,创建临时文件夹
conn = sqlite3.connect(db_filename)
print('Creating schema')
with open(schema_filename, 'rt') as f:
    schema = f.read()
conn.executescript(schema)
if not os.path.exists(r&quot;resource/temp&quot;):
    os.makedirs(r&quot;resource/temp&quot;)

#开启多进程读取excle文件,具体业务在__read_excle中
pool = Pool(processes=os.cpu_count())
results = pool.map(__read_excle, [(r'resource/linkdata/' + i, i) for i in os.listdir(r'resource/linkdata/')])
pool.close()
pool.join()

#合并各进程返回的页面集,并插入数据库中
pages = set()
for i in results:
    pages.update(i)
conn.executemany(r'INSERT INTO url(host,page,url) VALUES (?,?,?)',
                 (i.split(r'//')[1][:-1].split(r'/', 1) + [i] for i in pages))
conn.commit()

#取出url与id的对应关系
result = conn.execute(r'SELECT id,url FROM url')
page_map = {}
for i in result:
    page_map[i[1]] = i[0]

#用取出的对应关系,将临时文件中的url指向转译为id指向后存入数据库
for parent, dirnames, filenames in os.walk(r'resource/temp'):
    for i in filenames:
        with open(parent + r'/' + i, 'rb') as f:
            page_link = pickle.load(f)
        os.remove(parent + r'/' + i)
        conn.executemany(r'INSERT INTO link(source,target) VALUES (?,?)',
                         [(page_map[source], page_map[target]) for source in page_link for target in
                          page_link[source]])
conn.commit()
conn.close()

def __read_excle(file):
import xlrd
import pickle

pages = set()
page_link = {}
table = xlrd.open_workbook(file[0]).sheets()[0]
for j in range(table.nrows):
    row = table.row_values(j)
    url = row[1]
    pages.add(url)
    page_link[url] = [i[1:-1] for i in row[2][1:-1].split(', ')]
    for k in page_link[url]:
        pages.add(k)
with open(r'resource/temp/' + file[1] + '.dat', 'wb') as f:
    pickle.dump(page_link, f)
return pages

def link_out(host, page):
conn = sqlite3.connect(db_filename)
result = conn.execute(‘SELECT url.url FROM link LEFT ‘
‘JOIN url ON url.id=link.target WHERE source IN ‘
‘(SELECT id FROM url WHERE host=? AND page=?)’, (host, page))
return [i[0] for i in result]

def link_in(host, page):
conn = sqlite3.connect(db_filename)
result = conn.execute(‘SELECT url.url FROM link LEFT ‘
‘JOIN url ON url.id=link.source WHERE target IN ‘
‘(SELECT id FROM url WHERE host=? AND page=?)’, (host, page))
return [i[0] for i in result]

def link_out_count(host):
conn = sqlite3.connect(db_filename)
result = conn.execute(‘SELECT url.host,count(*) FROM link LEFT ‘
‘JOIN url ON url.id=link.target WHERE source IN ‘
‘(SELECT id FROM url WHERE host=?) GROUP BY url.host’, (host,))
return list(result)

def link_in_count(host):
conn = sqlite3.connect(db_filename)
result = conn.execute(‘SELECT url.host,count(*) FROM link LEFT ‘
‘JOIN url ON url.id=link.source WHERE target IN ‘
‘(SELECT id FROM url WHERE host=?) GROUP BY url.host’, (host,))
return list(result)
[/python]

数据分析S109:分类词频分析

这次是在之前的词频分析的基础上,将词组根据词性分类后取出各种词性中的高频词。
[python]
author = ‘sail’
import jieba
import jieba.analyse
import jieba.posseg as posseg
import dataio
from collections import *

db_filename = r’resource/chat.db’

def analyse(usrid=None, name=None):
with open(r’resource/stopWordList.txt’, encoding=’utf-8’) as f:
black_list = [i for i in f.read().split(‘\n’)]
speak_list = dataio.someonechat(usrid, name)
words = [i for i in jieba.posseg.cut(‘ ‘.join([i[‘contents’] for i in speak_list])) if i.word not in black_list]
word_dict=defaultdict(list)
for i in words:
if i.flag != ‘eng’:
i.flag = i.flag[0]
word_dict[i.flag].append(i.word)
print(Counter(word_dict[‘v’]).most_common(10))
print(Counter(word_dict[‘n’]).most_common(10))
print(Counter(word_dict[‘a’]).most_common(10))
[/python]

数据分析S108:微信文章密度的影响

[python][/python]
def lcs(x, y):
for i in range(min(len(y), len(x))):
if x[i] != y[i]:
break
else:
i += 1
return x[:i]
df = pd.ExcelFile(r”resource/wxgzhdata.xlsx”).parse(u”3月”)
df = df.fillna(method=”pad”).dropna()
df.insert(0, ‘标题’, list(map(lambda x:x[:2],df[‘文章标题’])))
df.insert(len(df.columns), ‘距下一篇的天数’, df[‘文章标题’])
df = df.fillna(method=”pad”).dropna().set_index([‘文章标题’]).groupby(level =0).apply(lambda x: x.apply(
lambda y: {1: lambda z: z[-1],2:lambda z:len(z), None: lambda z: z.groupby(type).sum().values[0]}
{‘标题’: 1, ‘日期’: 1, ‘粉丝数’: 1,’距下一篇的天数’:2}.get(y.name))).sort([‘日期’])#.set_index([‘标题’])

for i in range(len(df[‘距下一篇的天数’])-1,0,-1):

df[‘距下一篇的天数’][i]=df[‘距下一篇的天数’][i-1]

day_list=list(df[‘距下一篇的天数’].values)
day_list.insert(0,np.nan)
day_list.pop(-1)
df.insert(len(df.columns),’距上一篇的天数’,value=day_list)

fields = [df.xs(‘总阅读人数’, axis=1), df.xs(‘初次打开阅读人数’, axis=1), df.xs(‘分享次数’, axis=1), df.xs(‘每日增粉人数’, axis=1),
df.xs(‘粉丝数’, axis=1)]
to_percent = lambda x: (x * 100).round(1)
expressions = [(‘初次打开率’, lambda f: f[1] / f[4]), (‘分享率’, lambda f: f[2] / f[0]), (‘分享拉粉率’, lambda f: f[3] / f[2]),
(‘增粉速率’, lambda f: f[3] / f[4]), ( ‘阅读涨粉率’, lambda f: f[3] / f[0]),
(‘传播涨粉率’, lambda f: f[3] / (f[0] - f[1])), (‘二次传播率’, lambda f: (f[0] - f[1]) / f[0])]
old_field_count = len(df.columns)-2

计算各字段的值,处理为percent后,依次插入datagrame中

for i in enumerate(expressions, start=old_field_count):
df.insert(i[0], i[1][0], (i[1]1).apply(to_percent))
df.set_index(‘距下一篇的天数’).dropna().groupby(level =0).mean().T[5:12].plot(kind=’barh’, figsize=(16, 9))
df.set_index(‘距上一篇的天数’).dropna().groupby(level =0).mean().T[5:12].plot(kind=’barh’, figsize=(16, 9))
groupby=df.groupby(lambda x:x[:2])
df=groupby.mean()
df.insert(5,’连载篇数’,groupby.count()[[‘标题’]])
df.set_index(‘连载篇数’).groupby(level=0).mean().T[5:12].plot(kind=’barh’, figsize=(16, 9))

df.dropna().set_index(‘距下一篇的天数’)

df.plot(kind=’barh’, figsize=(16, 9))

#
from pylab import mpl
mpl.rcParams[‘font.sans-serif’] = [‘SimHei’] # 指定默认字体
mpl.rcParams[‘axes.unicode_minus’] = False # 解决保存图像是负号’-‘显示为方块的问题
plt.show()
[/python」

java8 lambda表达式体验

java8在去年就发布了,其中包含了接口的默认实现,重复注解等许多新特性。其中,最令人关心的莫过于支持lambda表达式了。由于项目的历史问题,我没能在第一时间尝试java8所带来的lambda表达式。
今天群里面有人提到需要把一个数组中所有只出现了一次的字符串去除,剩下的输出。这个过程用js或是python都能够很容易的完成,并且代码非常简短,但是如果用java的话就会变得比较复杂。所以我尝试了用lambda来进行简化。
下面代码是首先将数组转化为stream后分组,然后通过values取得分组完毕的字符串。再转化为stream后通过filter筛去只出现过一次的字符串。此时得到的应该是一个双层的list,于是foreach两次对每个字符串使用println,打印输出。
[java]
String[] strings = {"a", "a", "b", "c", "d", "d", "d"};
Stream.of(strings).collect(Collectors.groupingBy((x) -> x)).values().stream().filter((x) -> x.size() != 1).forEach((x) -> x.forEach(System.out::println));
[/java]

数据分析S107:完成微信运营评估模型

[python]
author = ‘sail’
import pandas as pd
from functools import reduce
import matplotlib.pyplot as plt

def analyse():

# 该函数用于将一个dataFrame合并为一条记录
# 入参x为一个dataFrame,调用tt对每个Series分别进行合并
def t(x):
    return x.apply(tt)

# 该函数用于对一个Series进行合并,入参x为一个Series
def tt(x):
    # 当x为标题时用reduce调用lcs函数求从字符串头开始的最长公共子串
    if x.name == '标题':
        return reduce(lcs, x)
    # 当x为下列内容是取最后一项
    elif x.name in {'日期', '粉丝数'}:
        return x[-1]
    # 其他情况时对将x中所有数据合并为一组求和,并返回该组求和结果
    else:
        return x.groupby(lambda x: 1).sum().values[0]

def lcs(x, y):
    for i in range(min(len(y), len(x))):
        if x[i] != y[i]:
            break
    else:
        i += 1
    return x[:i]

df = pd.ExcelFile(r&quot;resource/wxgzhdata.xlsx&quot;).parse(u&quot;3月&quot;)
df.insert(0, '标题', df['文章标题'])
df = df.fillna(method=&quot;pad&quot;).dropna().set_index(['文章标题']).groupby(lambda x: x[:2]).apply(lambda x: x.apply(
    lambda y: {1: lambda z: reduce(lcs, z), 2: lambda z: z[-1], None: lambda z: z.groupby(type).sum().values[0]}
    [{'标题': 1, '日期': 2, '粉丝数': 2}.get(y.name)](y))).set_index(['标题'])
# 下面为上句的另一种表达方式
# 用fillna填充nan
# df=df.fillna(method=&quot;pad&quot;)
# 将开头未能被pad填充的nan去除
# df=df.dropna()
# 将标题设为index以方便分组
# df=df.set_index(['文章标题'])
# 以标题的前两个字为基准分组
# df=df.groupby(lambda x: x[:2])
# 利用函数t对每个groupBy进行合并
# df=df.apply(t)
# 将各系列内文章标题的相同部分作为系列名称
# df=df.set_index(['标题'])

# 从dataframe中取得之后计算需要的Series
fields = [df.xs('总阅读人数', axis=1), df.xs('初次打开阅读人数', axis=1), df.xs('分享次数', axis=1), df.xs('每日增粉人数', axis=1),
          df.xs('粉丝数', axis=1)]
to_percent = lambda x: (x * 100).round(1)
# 各字段计算公式
expressions = [('初次打开率', lambda f: f[1] / f[4]), ('分享率', lambda f: f[2] / f[0]), ('分享拉粉率', lambda f: f[3] / f[2]),
               ('增粉速率', lambda f: f[3] / f[4]), ( '阅读涨粉率', lambda f: f[3] / f[0]),
               ('传播涨粉率', lambda f: f[3] / (f[0] - f[1])), ('二次传播率', lambda f: (f[0] - f[1]) / f[0])]
# 记录现有字段数量,用于下面计算新字段插入位置以及删除旧字段。
old_field_count = len(df.columns)
# 计算各字段的值,处理为percent后,依次插入datagrame中
for i in enumerate(expressions, start=old_field_count):
    df.insert(i[0], i[1][0], (i[1][1](fields)).apply(to_percent))
# 转制后将原始字段去除
df = df.T[old_field_count:]

df.plot(kind='barh', figsize=(16, 9))

from pylab import mpl
mpl.rcParams['font.sans-serif'] = ['SimHei']  # 指定默认字体
mpl.rcParams['axes.unicode_minus'] = False  # 解决保存图像是负号'-'显示为方块的问题
plt.show()

[/python]

python实现lambda-switch-lambda结构,向lambda内插入分支结构

python没有switch的问题由来已久,大家也用各种方式实现了switch语句。最常用的便是利用字典来实现。
而通过将单行带default功能的switch语句嵌入lambda中,可以实现根据key值不同,执行不同的分支代码的功能。

[python]

1.这是这次的实验数据,目标是根据前面的符号对后面的数据进行操作,问号或其他无法识别的符号则视为直接返回该数字,分别执行 22, 3*3, -4, 5,得到结果应该是[4, 27, -4, 5]

items = [(‘‘, 2), (‘*‘, 3), (‘-‘, 4), (‘?’, 5)]

2.从简单的做起首先用map遍历item生成[‘‘, ‘*‘, ‘-‘, ‘?’]的结果「我更喜欢列表解析,但是为了展示lambda-switch-lambda还是用map吧」。

items = list(map(lambda x: x[0], items))

3.然后加入带default功能的switch实现,得到将list转为[‘q’, ‘w’, ‘e’, ‘r’]

items = list(map(lambda x: {‘‘: ‘q’, ‘*‘: ‘w’, ‘-‘: ‘e’}.get(x[0], ‘r’), items))

4.最后将switch中的元素换成lambda并且在get之后执行,即可分别执行乘,幂,负和保持不变等分支,得到[4, 27, -4, 5]

items = list(map(lambda x: {‘‘: lambda y: y y, ‘‘: lambda y: y y, ‘-‘: lambda y: -y}.get(x[0], lambda y:y)(x[1]), items))

5.这是另外一种实现方式,相比上一种多加了一个dict可以对key进行一次转化,合并同类作用的key,实现switch中多个case公用一个代码块的功能。

下面是把’‘和’*‘都视为乘,可以得到[4, 9, -4, 5]

items = list(map(lambda x: {1: lambda y: y y, 2: lambda y: y ** y, 3: lambda y: -y, None: lambda y: y}[{‘‘: 1, ‘**’: 1, ‘-‘: 3}.get(x0]), items))

6.这是将第5条加入的dict改为通过len对key进行预处理,以此类推,在主dict后的[]中可以进行各种复杂操作,甚至再嵌入lambda,以实现更复杂的功能。

下面是根据key的长度,长度为1的执行乘操作,长度为2的执行幂操作,得到结果为[4, 27, 16, 25]

items = list(map(lambda x: {1: lambda y: y y, 2: lambda y: y * y, 3: lambda y: -y, None: lambda y: y}len(x[0]), items))
[/python]

利用该技巧,可以实现在lambda中加入分支语句,一定程度的改善了python中lambda的可用性。